]> git.lizzy.rs Git - dragonfireclient.git/blob - builtin/mainmenu/dlg_settings_advanced.lua
c16e4aad00c8a5839191728a87c2014e5ed42b1f
[dragonfireclient.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                                 --[[~ "defaults" is a noise parameter flag.
643                                 It describes the default processing options
644                                 for noise settings in main menu -> "All Settings". ]]
645                                 .. fgettext("defaults") .. ";" -- defaults
646                                 .. tostring(flags["defaults"] == true) .. "]" -- to get false if nil
647                                 .. "checkbox[5," .. height - 0.6 .. ";cb_eased;"
648                                 --[[~ "eased" is a noise parameter flag.
649                                 It is used to make the map smoother and
650                                 can be enabled in noise settings in
651                                 main menu -> "All Settings". ]]
652                                 .. fgettext("eased") .. ";" -- eased
653                                 .. tostring(flags["eased"] == true) .. "]"
654                                 .. "checkbox[5," .. height - 0.15 .. ";cb_absvalue;"
655                                 --[[~ "absvalue" is a noise parameter flag.
656                                 It is short for "absolute value".
657                                 It can be enabled in noise settings in
658                                 main menu -> "All Settings". ]]
659                                 .. fgettext("absvalue") .. ";" -- absvalue
660                                 .. tostring(flags["absvalue"] == true) .. "]"
661                 height = height + 1
662
663         elseif setting.type == "v3f" then
664                 local val = get_current_value(setting)
665                 local v3f = {}
666                 for line in val:gmatch("[+-]?[%d.-e]+") do -- All numeric characters
667                         table.insert(v3f, line)
668                 end
669
670                 height = height + 0.3
671                 formspec = formspec
672                                 .. "field[0.3," .. height .. ";3.3,1;te_x;"
673                                 .. fgettext("X") .. ";" -- X
674                                 .. core.formspec_escape(v3f[1] or "") .. "]"
675                                 .. "field[3.6," .. height .. ";3.3,1;te_y;"
676                                 .. fgettext("Y") .. ";" -- Y
677                                 .. core.formspec_escape(v3f[2] or "") .. "]"
678                                 .. "field[6.9," .. height .. ";3.3,1;te_z;"
679                                 .. fgettext("Z") .. ";" -- Z
680                                 .. core.formspec_escape(v3f[3] or "") .. "]"
681                 height = height + 1.1
682
683         elseif setting.type == "flags" then
684                 local current_flags = flags_to_table(get_current_value(setting))
685                 local flags = {}
686                 for _, name in ipairs(current_flags) do
687                         -- Index by name, to avoid iterating over all enabled_flags for every possible flag.
688                         if name:sub(1, 2) == "no" then
689                                 flags[name:sub(3)] = false
690                         else
691                                 flags[name] = true
692                         end
693                 end
694                 local flags_count = #setting.possible / 2
695                 local max_height = math.ceil(flags_count / 2) / 2
696
697                 -- More space for flags
698                 description_height = description_height - 1
699                 height = height - 1
700
701                 local fields = {} -- To build formspec
702                 local j = 1
703                 for _, name in ipairs(setting.possible) do
704                         if name:sub(1, 2) ~= "no" then
705                                 local x = 0.5
706                                 local y = height + j / 2 - 0.75
707                                 if j - 1 >= flags_count / 2 then -- 2nd column
708                                         x = 5
709                                         y = y - max_height
710                                 end
711                                 j = j + 1;
712                                 local checkbox_name = "cb_" .. name
713                                 local is_enabled = flags[name] == true -- to get false if nil
714                                 checkboxes[checkbox_name] = is_enabled
715
716                                 fields[#fields + 1] = ("checkbox[%f,%f;%s;%s;%s]"):format(
717                                         x, y, checkbox_name, name, tostring(is_enabled)
718                                 )
719                         end
720                 end
721                 formspec = table.concat(fields)
722                 height = height + max_height + 0.25
723
724         else
725                 -- TODO: fancy input for float, int
726                 local text = get_current_value(setting)
727                 if dialogdata.error_message and dialogdata.entered_text then
728                         text = dialogdata.entered_text
729                 end
730                 formspec = "field[0.28," .. height + 0.15 .. ";" .. width .. ",1;te_setting_value;;"
731                                 .. core.formspec_escape(text) .. "]"
732                 height = height + 1.15
733         end
734
735         -- Box good, textarea bad. Calculate textarea size from box.
736         local function create_textfield(size, label, text, bg_color)
737                 local textarea = {
738                         x = size.x + 0.3,
739                         y = size.y,
740                         w = size.w + 0.25,
741                         h = size.h * 1.16 + 0.12
742                 }
743                 return ("box[%f,%f;%f,%f;%s]textarea[%f,%f;%f,%f;;%s;%s]"):format(
744                         size.x, size.y, size.w, size.h, bg_color or "#000",
745                         textarea.x, textarea.y, textarea.w, textarea.h,
746                         core.formspec_escape(label), core.formspec_escape(text)
747                 )
748
749         end
750
751         -- When there's an error: Shrink description textarea and add error below
752         if dialogdata.error_message then
753                 local error_box = {
754                         x = 0,
755                         y = description_height - 0.4,
756                         w = width - 0.25,
757                         h = 0.5
758                 }
759                 formspec = formspec ..
760                         create_textfield(error_box, "", dialogdata.error_message, "#600")
761                 description_height = description_height - 0.75
762         end
763
764         -- Get description field
765         local description_box = {
766                 x = 0,
767                 y = 0.2,
768                 w = width - 0.25,
769                 h = description_height
770         }
771
772         local setting_name = setting.name
773         if setting.readable_name then
774                 setting_name = fgettext_ne(setting.readable_name) ..
775                         " (" .. setting.name .. ")"
776         end
777
778         local comment_text
779         if setting.comment == "" then
780                 comment_text = fgettext_ne("(No description of setting given)")
781         else
782                 comment_text = fgettext_ne(setting.comment)
783         end
784
785         return (
786                 "size[" .. width .. "," .. height + 0.25 .. ",true]" ..
787                 create_textfield(description_box, setting_name, comment_text) ..
788                 formspec ..
789                 "button[" .. width / 2 - 2.5 .. "," .. height - 0.4 .. ";2.5,1;btn_done;" ..
790                         fgettext("Save") .. "]" ..
791                 "button[" .. width / 2 .. "," .. height - 0.4 .. ";2.5,1;btn_cancel;" ..
792                         fgettext("Cancel") .. "]"
793         )
794 end
795
796 local function handle_change_setting_buttons(this, fields)
797         local setting = settings[selected_setting]
798         if fields["btn_done"] or fields["key_enter"] then
799                 if setting.type == "bool" then
800                         local new_value = fields["dd_setting_value"]
801                         -- Note: new_value is the actual (translated) value shown in the dropdown
802                         core.settings:set_bool(setting.name, new_value == fgettext("Enabled"))
803
804                 elseif setting.type == "enum" then
805                         local new_value = fields["dd_setting_value"]
806                         core.settings:set(setting.name, new_value)
807
808                 elseif setting.type == "int" then
809                         local new_value = tonumber(fields["te_setting_value"])
810                         if not new_value or math.floor(new_value) ~= new_value then
811                                 this.data.error_message = fgettext_ne("Please enter a valid integer.")
812                                 this.data.entered_text = fields["te_setting_value"]
813                                 core.update_formspec(this:get_formspec())
814                                 return true
815                         end
816                         if setting.min and new_value < setting.min then
817                                 this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
818                                 this.data.entered_text = fields["te_setting_value"]
819                                 core.update_formspec(this:get_formspec())
820                                 return true
821                         end
822                         if setting.max and new_value > setting.max then
823                                 this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
824                                 this.data.entered_text = fields["te_setting_value"]
825                                 core.update_formspec(this:get_formspec())
826                                 return true
827                         end
828                         core.settings:set(setting.name, new_value)
829
830                 elseif setting.type == "float" then
831                         local new_value = tonumber(fields["te_setting_value"])
832                         if not new_value then
833                                 this.data.error_message = fgettext_ne("Please enter a valid number.")
834                                 this.data.entered_text = fields["te_setting_value"]
835                                 core.update_formspec(this:get_formspec())
836                                 return true
837                         end
838                         if setting.min and new_value < setting.min then
839                                 this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min)
840                                 this.data.entered_text = fields["te_setting_value"]
841                                 core.update_formspec(this:get_formspec())
842                                 return true
843                         end
844                         if setting.max and new_value > setting.max then
845                                 this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max)
846                                 this.data.entered_text = fields["te_setting_value"]
847                                 core.update_formspec(this:get_formspec())
848                                 return true
849                         end
850                         core.settings:set(setting.name, new_value)
851
852                 elseif setting.type == "flags" then
853                         local values = {}
854                         for _, name in ipairs(setting.possible) do
855                                 if name:sub(1, 2) ~= "no" then
856                                         if checkboxes["cb_" .. name] then
857                                                 table.insert(values, name)
858                                         else
859                                                 table.insert(values, "no" .. name)
860                                         end
861                                 end
862                         end
863
864                         checkboxes = {}
865
866                         local new_value = table.concat(values, ", ")
867                         core.settings:set(setting.name, new_value)
868
869                 elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then
870                         local np_flags = {}
871                         for _, name in ipairs(setting.flags) do
872                                 if checkboxes["cb_" .. name] then
873                                         table.insert(np_flags, name)
874                                 end
875                         end
876
877                         checkboxes = {}
878
879                         if setting.type == "noise_params_2d" then
880                                  fields["te_spready"] = fields["te_spreadz"]
881                         end
882                         local new_value = {
883                                 offset = fields["te_offset"],
884                                 scale = fields["te_scale"],
885                                 spread = {
886                                         x = fields["te_spreadx"],
887                                         y = fields["te_spready"],
888                                         z = fields["te_spreadz"]
889                                 },
890                                 seed = fields["te_seed"],
891                                 octaves = fields["te_octaves"],
892                                 persistence = fields["te_persist"],
893                                 lacunarity = fields["te_lacun"],
894                                 flags = table.concat(np_flags, ", ")
895                         }
896                         core.settings:set_np_group(setting.name, new_value)
897
898                 elseif setting.type == "v3f" then
899                         local new_value = "("
900                                         .. fields["te_x"] .. ", "
901                                         .. fields["te_y"] .. ", "
902                                         .. fields["te_z"] .. ")"
903                         core.settings:set(setting.name, new_value)
904
905                 else
906                         local new_value = fields["te_setting_value"]
907                         core.settings:set(setting.name, new_value)
908                 end
909                 core.settings:write()
910                 this:delete()
911                 return true
912         end
913
914         if fields["btn_cancel"] then
915                 this:delete()
916                 return true
917         end
918
919         if fields["btn_browser_path"] then
920                 core.show_path_select_dialog("dlg_browse_path",
921                         fgettext_ne("Select directory"), false)
922         end
923
924         if fields["btn_browser_filepath"] then
925                 core.show_path_select_dialog("dlg_browse_path",
926                         fgettext_ne("Select file"), true)
927         end
928
929         if fields["dlg_browse_path_accepted"] then
930                 this.data.selected_path = fields["dlg_browse_path_accepted"]
931                 core.update_formspec(this:get_formspec())
932         end
933
934         if setting.type == "flags"
935                         or setting.type == "noise_params_2d"
936                         or setting.type == "noise_params_3d" then
937                 for name, value in pairs(fields) do
938                         if name:sub(1, 3) == "cb_" then
939                                 checkboxes[name] = value == "true"
940                         end
941                 end
942         end
943
944         return false
945 end
946
947 local function create_settings_formspec(tabview, _, tabdata)
948         local formspec = "size[12,5.4;true]" ..
949                         "tablecolumns[color;tree;text,width=28;text]" ..
950                         "tableoptions[background=#00000000;border=false]" ..
951                         "field[0.3,0.1;10.2,1;search_string;;" .. core.formspec_escape(search_string) .. "]" ..
952                         "field_close_on_enter[search_string;false]" ..
953                         "button[10.2,-0.2;2,1;search;" .. fgettext("Search") .. "]" ..
954                         "table[0,0.8;12,3.5;list_settings;"
955
956         local current_level = 0
957         for _, entry in ipairs(settings) do
958                 local name
959                 if not core.settings:get_bool("main_menu_technical_settings") and entry.readable_name then
960                         name = fgettext_ne(entry.readable_name)
961                 else
962                         name = entry.name
963                 end
964
965                 if entry.type == "category" then
966                         current_level = entry.level
967                         formspec = formspec .. "#FFFF00," .. current_level .. "," .. fgettext(name) .. ",,"
968
969                 elseif entry.type == "bool" then
970                         local value = get_current_value(entry)
971                         if core.is_yes(value) then
972                                 value = fgettext("Enabled")
973                         else
974                                 value = fgettext("Disabled")
975                         end
976                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
977                                         .. value .. ","
978
979                 elseif entry.type == "key" then --luacheck: ignore
980                         -- ignore key settings, since we have a special dialog for them
981
982                 elseif entry.type == "noise_params_2d" or entry.type == "noise_params_3d" then
983                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
984                                         .. core.formspec_escape(get_current_np_group_as_string(entry)) .. ","
985
986                 else
987                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
988                                         .. core.formspec_escape(get_current_value(entry)) .. ","
989                 end
990         end
991
992         if #settings > 0 then
993                 formspec = formspec:sub(1, -2) -- remove trailing comma
994         end
995         formspec = formspec .. ";" .. selected_setting .. "]" ..
996                         "button[0,4.9;4,1;btn_back;".. fgettext("< Back to Settings page") .. "]" ..
997                         "button[10,4.9;2,1;btn_edit;" .. fgettext("Edit") .. "]" ..
998                         "button[7,4.9;3,1;btn_restore;" .. fgettext("Restore Default") .. "]" ..
999                         "checkbox[0,4.3;cb_tech_settings;" .. fgettext("Show technical names") .. ";"
1000                                         .. dump(core.settings:get_bool("main_menu_technical_settings")) .. "]"
1001
1002         return formspec
1003 end
1004
1005 local function handle_settings_buttons(this, fields, tabname, tabdata)
1006         local list_enter = false
1007         if fields["list_settings"] then
1008                 selected_setting = core.get_table_index("list_settings")
1009                 if core.explode_table_event(fields["list_settings"]).type == "DCL" then
1010                         -- Directly toggle booleans
1011                         local setting = settings[selected_setting]
1012                         if setting and setting.type == "bool" then
1013                                 local current_value = get_current_value(setting)
1014                                 core.settings:set_bool(setting.name, not core.is_yes(current_value))
1015                                 core.settings:write()
1016                                 return true
1017                         else
1018                                 list_enter = true
1019                         end
1020                 else
1021                         return true
1022                 end
1023         end
1024
1025         if fields.search or fields.key_enter_field == "search_string" then
1026                 if search_string == fields.search_string then
1027                         if selected_setting > 0 then
1028                                 -- Go to next result on enter press
1029                                 local i = selected_setting + 1
1030                                 local looped = false
1031                                 while i > #settings or settings[i].type == "category" do
1032                                         i = i + 1
1033                                         if i > #settings then
1034                                                 -- Stop infinte looping
1035                                                 if looped then
1036                                                         return false
1037                                                 end
1038                                                 i = 1
1039                                                 looped = true
1040                                         end
1041                                 end
1042                                 selected_setting = i
1043                                 core.update_formspec(this:get_formspec())
1044                                 return true
1045                         end
1046                 else
1047                         -- Search for setting
1048                         search_string = fields.search_string
1049                         settings, selected_setting = filter_settings(full_settings, search_string)
1050                         core.update_formspec(this:get_formspec())
1051                 end
1052                 return true
1053         end
1054
1055         if fields["btn_edit"] or list_enter then
1056                 local setting = settings[selected_setting]
1057                 if setting and setting.type ~= "category" then
1058                         local edit_dialog = dialog_create("change_setting",
1059                                         create_change_setting_formspec, handle_change_setting_buttons)
1060                         edit_dialog:set_parent(this)
1061                         this:hide()
1062                         edit_dialog:show()
1063                 end
1064                 return true
1065         end
1066
1067         if fields["btn_restore"] then
1068                 local setting = settings[selected_setting]
1069                 if setting and setting.type ~= "category" then
1070                         core.settings:remove(setting.name)
1071                         core.settings:write()
1072                         core.update_formspec(this:get_formspec())
1073                 end
1074                 return true
1075         end
1076
1077         if fields["btn_back"] then
1078                 this:delete()
1079                 return true
1080         end
1081
1082         if fields["cb_tech_settings"] then
1083                 core.settings:set("main_menu_technical_settings", fields["cb_tech_settings"])
1084                 core.settings:write()
1085                 core.update_formspec(this:get_formspec())
1086                 return true
1087         end
1088
1089         return false
1090 end
1091
1092 function create_adv_settings_dlg()
1093         local dlg = dialog_create("settings_advanced",
1094                                 create_settings_formspec,
1095                                 handle_settings_buttons,
1096                                 nil)
1097
1098                                 return dlg
1099 end
1100
1101 -- Uncomment to generate 'minetest.conf.example' and 'settings_translation_file.cpp'.
1102 -- For RUN_IN_PLACE the generated files may appear in the 'bin' folder.
1103 -- See comment and alternative line at the end of 'generate_from_settingtypes.lua'.
1104
1105 --assert(loadfile(core.get_builtin_path().."mainmenu"..DIR_DELIM..
1106 --              "generate_from_settingtypes.lua"))(parse_config_file(true, false))