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