components.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. --Luanti
  2. --Copyright (C) 2022 rubenwardy
  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. local make = {}
  18. -- This file defines various component constructors, of the form:
  19. --
  20. -- make.component(setting)
  21. --
  22. -- `setting` is a table representing the settingtype.
  23. --
  24. -- A component is a table with the following:
  25. --
  26. -- * `full_width`: (Optional) true if the component shouldn't reserve space for info / reset.
  27. -- * `info_text`: (Optional) string, informational text shown in an info icon.
  28. -- * `setting`: (Optional) the setting.
  29. -- * `max_w`: (Optional) maximum width, `avail_w` will never exceed this.
  30. -- * `resettable`: (Optional) if this is true, a reset button is shown.
  31. -- * `get_formspec = function(self, avail_w)`:
  32. -- * `avail_w` is the available width for the component.
  33. -- * Returns `fs, used_height`.
  34. -- * `fs` is a string for the formspec.
  35. -- Components should be relative to `0,0`, and not exceed `avail_w` or the returned `used_height`.
  36. -- * `used_height` is the space used by components in `fs`.
  37. -- * `on_submit = function(self, fields, parent)`:
  38. -- * `fields`: submitted formspec fields
  39. -- * `parent`: the fstk element for the settings UI, use to show dialogs
  40. -- * Return true if the event was handled, to prevent future components receiving it.
  41. local function get_label(setting)
  42. local show_technical_names = core.settings:get_bool("show_technical_names")
  43. if not show_technical_names and setting.readable_name then
  44. return fgettext(setting.readable_name)
  45. end
  46. return setting.name
  47. end
  48. local function is_valid_number(value)
  49. return type(value) == "number" and not (value ~= value or value >= math.huge or value <= -math.huge)
  50. end
  51. function make.heading(text)
  52. return {
  53. full_width = true,
  54. get_formspec = function(self, avail_w)
  55. return ("label[0,0.6;%s]box[0,0.9;%f,0.05;#ccc6]"):format(core.formspec_escape(text), avail_w), 1.2
  56. end,
  57. }
  58. end
  59. function make.note(text)
  60. return {
  61. full_width = true,
  62. get_formspec = function(self, avail_w)
  63. -- Assuming label height 0.4:
  64. -- Position at y=0 to eat 0.2 of the padding above, leave 0.05.
  65. -- The returned used_height doesn't include padding.
  66. return ("label[0,0;%s]"):format(core.colorize("#bbb", core.formspec_escape(text))), 0.2
  67. end,
  68. }
  69. end
  70. --- Used for string and numeric style fields
  71. ---
  72. --- @param converter Function to coerce values from strings.
  73. --- @param validator Validator function, optional. Returns true when valid.
  74. --- @param stringifier Function to convert values to strings, optional.
  75. local function make_field(converter, validator, stringifier)
  76. return function(setting)
  77. return {
  78. info_text = setting.comment,
  79. setting = setting,
  80. get_formspec = function(self, avail_w)
  81. local value = core.settings:get(setting.name) or setting.default
  82. self.resettable = core.settings:has(setting.name)
  83. local fs = ("field[0,0.3;%f,0.8;%s;%s;%s]"):format(
  84. avail_w - 1.5, setting.name, get_label(setting), core.formspec_escape(value))
  85. fs = fs .. ("field_enter_after_edit[%s;true]"):format(setting.name)
  86. fs = fs .. ("button[%f,0.3;1.5,0.8;%s;%s]"):format(avail_w - 1.5, "set_" .. setting.name, fgettext("Set"))
  87. return fs, 1.1
  88. end,
  89. on_submit = function(self, fields)
  90. if fields["set_" .. setting.name] or fields.key_enter_field == setting.name then
  91. local value = converter(fields[setting.name])
  92. if value == nil or (validator and not validator(value)) then
  93. return true
  94. end
  95. if setting.min then
  96. value = math.max(value, setting.min)
  97. end
  98. if setting.max then
  99. value = math.min(value, setting.max)
  100. end
  101. core.settings:set(setting.name, (stringifier or tostring)(value))
  102. return true
  103. end
  104. end,
  105. }
  106. end
  107. end
  108. make.float = make_field(tonumber, is_valid_number, function(x)
  109. local str = tostring(x)
  110. if str:match("^[+-]?%d+$") then
  111. str = str .. ".0"
  112. end
  113. return str
  114. end)
  115. make.int = make_field(function(x)
  116. local value = tonumber(x)
  117. return value and math.floor(value)
  118. end, is_valid_number)
  119. make.string = make_field(tostring, nil)
  120. function make.bool(setting)
  121. return {
  122. info_text = setting.comment,
  123. setting = setting,
  124. get_formspec = function(self, avail_w)
  125. local value = core.settings:get_bool(setting.name, core.is_yes(setting.default))
  126. self.resettable = core.settings:has(setting.name)
  127. local fs = ("checkbox[0,0.25;%s;%s;%s]"):format(
  128. setting.name, get_label(setting), tostring(value))
  129. return fs, 0.5
  130. end,
  131. on_submit = function(self, fields)
  132. if fields[setting.name] == nil then
  133. return false
  134. end
  135. core.settings:set_bool(setting.name, core.is_yes(fields[setting.name]))
  136. return true
  137. end,
  138. }
  139. end
  140. function make.enum(setting)
  141. return {
  142. info_text = setting.comment,
  143. setting = setting,
  144. max_w = 4.5,
  145. get_formspec = function(self, avail_w)
  146. local value = core.settings:get(setting.name) or setting.default
  147. self.resettable = core.settings:has(setting.name)
  148. local labels = setting.option_labels or {}
  149. local items = {}
  150. for i, option in ipairs(setting.values) do
  151. items[i] = core.formspec_escape(labels[option] or option)
  152. end
  153. local selected_idx = table.indexof(setting.values, value)
  154. local fs = "label[0,0.1;" .. get_label(setting) .. "]"
  155. fs = fs .. ("dropdown[0,0.3;%f,0.8;%s;%s;%d;true]"):format(
  156. avail_w, setting.name, table.concat(items, ","), selected_idx, value)
  157. return fs, 1.1
  158. end,
  159. on_submit = function(self, fields)
  160. local old_value = core.settings:get(setting.name) or setting.default
  161. local idx = tonumber(fields[setting.name]) or 0
  162. local value = setting.values[idx]
  163. if value == nil or value == old_value then
  164. return false
  165. end
  166. core.settings:set(setting.name, value)
  167. return true
  168. end,
  169. }
  170. end
  171. local function make_path(setting)
  172. return {
  173. info_text = setting.comment,
  174. setting = setting,
  175. get_formspec = function(self, avail_w)
  176. local value = core.settings:get(setting.name) or setting.default
  177. self.resettable = core.settings:has(setting.name)
  178. local fs = ("field[0,0.3;%f,0.8;%s;%s;%s]"):format(
  179. avail_w - 3, setting.name, get_label(setting), core.formspec_escape(value))
  180. fs = fs .. ("button[%f,0.3;1.5,0.8;%s;%s]"):format(avail_w - 3, "pick_" .. setting.name, fgettext("Browse"))
  181. fs = fs .. ("button[%f,0.3;1.5,0.8;%s;%s]"):format(avail_w - 1.5, "set_" .. setting.name, fgettext("Set"))
  182. return fs, 1.1
  183. end,
  184. on_submit = function(self, fields)
  185. local dialog_name = "dlg_path_" .. setting.name
  186. if fields["pick_" .. setting.name] then
  187. local is_file = setting.type ~= "path"
  188. core.show_path_select_dialog(dialog_name,
  189. is_file and fgettext_ne("Select file") or fgettext_ne("Select directory"), is_file)
  190. return true
  191. end
  192. if fields[dialog_name .. "_accepted"] then
  193. local value = fields[dialog_name .. "_accepted"]
  194. if value ~= nil then
  195. core.settings:set(setting.name, value)
  196. end
  197. return true
  198. end
  199. if fields["set_" .. setting.name] or fields.key_enter_field == setting.name then
  200. local value = fields[setting.name]
  201. if value ~= nil then
  202. core.settings:set(setting.name, value)
  203. end
  204. return true
  205. end
  206. end,
  207. }
  208. end
  209. if PLATFORM == "Android" then
  210. -- The Irrlicht file picker doesn't work on Android.
  211. make.path = make.string
  212. make.filepath = make.string
  213. else
  214. make.path = make_path
  215. make.filepath = make_path
  216. end
  217. function make.v3f(setting)
  218. return {
  219. info_text = setting.comment,
  220. setting = setting,
  221. get_formspec = function(self, avail_w)
  222. local value = vector.from_string(core.settings:get(setting.name) or setting.default)
  223. self.resettable = core.settings:has(setting.name)
  224. -- Allocate space for "Set" button
  225. avail_w = avail_w - 1
  226. local fs = "label[0,0.1;" .. get_label(setting) .. "]"
  227. local field_width = (avail_w - 3*0.25) / 3
  228. fs = fs .. ("field[%f,0.6;%f,0.8;%s;%s;%s]"):format(
  229. 0, field_width, setting.name .. "_x", "X", value.x)
  230. fs = fs .. ("field[%f,0.6;%f,0.8;%s;%s;%s]"):format(
  231. field_width + 0.25, field_width, setting.name .. "_y", "Y", value.y)
  232. fs = fs .. ("field[%f,0.6;%f,0.8;%s;%s;%s]"):format(
  233. 2 * (field_width + 0.25), field_width, setting.name .. "_z", "Z", value.z)
  234. fs = fs .. ("button[%f,0.6;1,0.8;%s;%s]"):format(avail_w, "set_" .. setting.name, fgettext("Set"))
  235. return fs, 1.4
  236. end,
  237. on_submit = function(self, fields)
  238. if fields["set_" .. setting.name] or
  239. fields.key_enter_field == setting.name .. "_x" or
  240. fields.key_enter_field == setting.name .. "_y" or
  241. fields.key_enter_field == setting.name .. "_z" then
  242. local x = tonumber(fields[setting.name .. "_x"])
  243. local y = tonumber(fields[setting.name .. "_y"])
  244. local z = tonumber(fields[setting.name .. "_z"])
  245. if is_valid_number(x) and is_valid_number(y) and is_valid_number(z) then
  246. core.settings:set(setting.name, vector.new(x, y, z):to_string())
  247. else
  248. core.log("error", "Invalid vector: " .. dump({x, y, z}))
  249. end
  250. return true
  251. end
  252. end,
  253. }
  254. end
  255. function make.flags(setting)
  256. local checkboxes = {}
  257. return {
  258. info_text = setting.comment,
  259. setting = setting,
  260. get_formspec = function(self, avail_w)
  261. local fs = {
  262. "label[0,0.1;" .. get_label(setting) .. "]",
  263. }
  264. self.resettable = core.settings:has(setting.name)
  265. checkboxes = {}
  266. for _, name in ipairs(setting.possible) do
  267. checkboxes[name] = false
  268. end
  269. local function apply_flags(flag_string, what)
  270. local prefixed_flags = {}
  271. for _, name in ipairs(flag_string:split(",")) do
  272. prefixed_flags[name:trim()] = true
  273. end
  274. for _, name in ipairs(setting.possible) do
  275. local enabled = prefixed_flags[name]
  276. local disabled = prefixed_flags["no" .. name]
  277. if enabled and disabled then
  278. core.log("warning", "Flag " .. name .. " in " .. what .. " " ..
  279. setting.name .. " both enabled and disabled, ignoring")
  280. elseif enabled then
  281. checkboxes[name] = true
  282. elseif disabled then
  283. checkboxes[name] = false
  284. end
  285. end
  286. end
  287. -- First apply the default, which is necessary since flags
  288. -- which are not overridden may be missing from the value.
  289. apply_flags(setting.default, "default for setting")
  290. local value = core.settings:get(setting.name)
  291. if value then
  292. apply_flags(value, "setting")
  293. end
  294. local columns = math.max(math.floor(avail_w / 2.5), 1)
  295. local column_width = avail_w / columns
  296. local x = 0
  297. local y = 0.55
  298. for _, possible in ipairs(setting.possible) do
  299. if x >= avail_w then
  300. x = 0
  301. y = y + 0.5
  302. end
  303. local is_checked = checkboxes[possible]
  304. fs[#fs + 1] = ("checkbox[%f,%f;%s;%s;%s]"):format(
  305. x, y, setting.name .. "_" .. possible,
  306. core.formspec_escape(possible), tostring(is_checked))
  307. x = x + column_width
  308. end
  309. return table.concat(fs, ""), y + 0.25
  310. end,
  311. on_submit = function(self, fields)
  312. local changed = false
  313. for name, _ in pairs(checkboxes) do
  314. local value = fields[setting.name .. "_" .. name]
  315. if value ~= nil then
  316. checkboxes[name] = core.is_yes(value)
  317. changed = true
  318. end
  319. end
  320. if changed then
  321. local values = {}
  322. for _, name in ipairs(setting.possible) do
  323. if checkboxes[name] then
  324. table.insert(values, name)
  325. else
  326. table.insert(values, "no" .. name)
  327. end
  328. end
  329. core.settings:set(setting.name, table.concat(values, ","))
  330. end
  331. return changed
  332. end
  333. }
  334. end
  335. local function make_noise_params(setting)
  336. return {
  337. info_text = setting.comment,
  338. setting = setting,
  339. get_formspec = function(self, avail_w)
  340. -- The "defaults" noise parameter flag doesn't reset a noise
  341. -- setting to its default value, so we offer a regular reset button.
  342. self.resettable = core.settings:has(setting.name)
  343. local fs = "label[0,0.4;" .. get_label(setting) .. "]" ..
  344. ("button[%f,0;2.5,0.8;%s;%s]"):format(avail_w - 2.5, "edit_" .. setting.name, fgettext("Edit"))
  345. return fs, 0.8
  346. end,
  347. on_submit = function(self, fields, tabview)
  348. if fields["edit_" .. setting.name] then
  349. local dlg = create_change_mapgen_flags_dlg(setting)
  350. dlg:set_parent(tabview)
  351. tabview:hide()
  352. dlg:show()
  353. return true
  354. end
  355. end,
  356. }
  357. end
  358. make.noise_params_2d = make_noise_params
  359. make.noise_params_3d = make_noise_params
  360. return make