components.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. --Minetest
  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. --- Used for string and numeric style fields
  60. ---
  61. --- @param converter Function to coerce values from strings.
  62. --- @param validator Validator function, optional. Returns true when valid.
  63. --- @param stringifier Function to convert values to strings, optional.
  64. local function make_field(converter, validator, stringifier)
  65. return function(setting)
  66. return {
  67. info_text = setting.comment,
  68. setting = setting,
  69. get_formspec = function(self, avail_w)
  70. local value = core.settings:get(setting.name) or setting.default
  71. self.resettable = core.settings:has(setting.name)
  72. local fs = ("field[0,0.3;%f,0.8;%s;%s;%s]"):format(
  73. avail_w - 1.5, setting.name, get_label(setting), core.formspec_escape(value))
  74. fs = fs .. ("field_enter_after_edit[%s;true]"):format(setting.name)
  75. fs = fs .. ("button[%f,0.3;1.5,0.8;%s;%s]"):format(avail_w - 1.5, "set_" .. setting.name, fgettext("Set"))
  76. return fs, 1.1
  77. end,
  78. on_submit = function(self, fields)
  79. if fields["set_" .. setting.name] or fields.key_enter_field == setting.name then
  80. local value = converter(fields[setting.name])
  81. if value == nil or (validator and not validator(value)) then
  82. return true
  83. end
  84. if setting.min then
  85. value = math.max(value, setting.min)
  86. end
  87. if setting.max then
  88. value = math.min(value, setting.max)
  89. end
  90. core.settings:set(setting.name, (stringifier or tostring)(value))
  91. return true
  92. end
  93. end,
  94. }
  95. end
  96. end
  97. make.float = make_field(tonumber, is_valid_number, function(x)
  98. local str = tostring(x)
  99. if str:match("^[+-]?%d+$") then
  100. str = str .. ".0"
  101. end
  102. return str
  103. end)
  104. make.int = make_field(function(x)
  105. local value = tonumber(x)
  106. return value and math.floor(value)
  107. end, is_valid_number)
  108. make.string = make_field(tostring, nil)
  109. function make.bool(setting)
  110. return {
  111. info_text = setting.comment,
  112. setting = setting,
  113. get_formspec = function(self, avail_w)
  114. local value = core.settings:get_bool(setting.name, core.is_yes(setting.default))
  115. self.resettable = core.settings:has(setting.name)
  116. local fs = ("checkbox[0,0.25;%s;%s;%s]"):format(
  117. setting.name, get_label(setting), tostring(value))
  118. return fs, 0.5
  119. end,
  120. on_submit = function(self, fields)
  121. if fields[setting.name] == nil then
  122. return false
  123. end
  124. core.settings:set_bool(setting.name, core.is_yes(fields[setting.name]))
  125. return true
  126. end,
  127. }
  128. end
  129. function make.enum(setting)
  130. return {
  131. info_text = setting.comment,
  132. setting = setting,
  133. max_w = 4.5,
  134. get_formspec = function(self, avail_w)
  135. local value = core.settings:get(setting.name) or setting.default
  136. self.resettable = core.settings:has(setting.name)
  137. local labels = setting.option_labels or {}
  138. local items = {}
  139. for i, option in ipairs(setting.values) do
  140. items[i] = core.formspec_escape(labels[option] or option)
  141. end
  142. local selected_idx = table.indexof(setting.values, value)
  143. local fs = "label[0,0.1;" .. get_label(setting) .. "]"
  144. fs = fs .. ("dropdown[0,0.3;%f,0.8;%s;%s;%d;true]"):format(
  145. avail_w, setting.name, table.concat(items, ","), selected_idx, value)
  146. return fs, 1.1
  147. end,
  148. on_submit = function(self, fields)
  149. local old_value = core.settings:get(setting.name) or setting.default
  150. local idx = tonumber(fields[setting.name]) or 0
  151. local value = setting.values[idx]
  152. if value == nil or value == old_value then
  153. return false
  154. end
  155. core.settings:set(setting.name, value)
  156. return true
  157. end,
  158. }
  159. end
  160. local function make_path(setting)
  161. return {
  162. info_text = setting.comment,
  163. setting = setting,
  164. get_formspec = function(self, avail_w)
  165. local value = core.settings:get(setting.name) or setting.default
  166. self.resettable = core.settings:has(setting.name)
  167. local fs = ("field[0,0.3;%f,0.8;%s;%s;%s]"):format(
  168. avail_w - 3, setting.name, get_label(setting), core.formspec_escape(value))
  169. fs = fs .. ("button[%f,0.3;1.5,0.8;%s;%s]"):format(avail_w - 3, "pick_" .. setting.name, fgettext("Browse"))
  170. fs = fs .. ("button[%f,0.3;1.5,0.8;%s;%s]"):format(avail_w - 1.5, "set_" .. setting.name, fgettext("Set"))
  171. return fs, 1.1
  172. end,
  173. on_submit = function(self, fields)
  174. local dialog_name = "dlg_path_" .. setting.name
  175. if fields["pick_" .. setting.name] then
  176. local is_file = setting.type ~= "path"
  177. core.show_path_select_dialog(dialog_name,
  178. is_file and fgettext_ne("Select file") or fgettext_ne("Select directory"), is_file)
  179. return true
  180. end
  181. if fields[dialog_name .. "_accepted"] then
  182. local value = fields[dialog_name .. "_accepted"]
  183. if value ~= nil then
  184. core.settings:set(setting.name, value)
  185. end
  186. return true
  187. end
  188. if fields["set_" .. setting.name] or fields.key_enter_field == setting.name then
  189. local value = fields[setting.name]
  190. if value ~= nil then
  191. core.settings:set(setting.name, value)
  192. end
  193. return true
  194. end
  195. end,
  196. }
  197. end
  198. if PLATFORM == "Android" then
  199. -- The Irrlicht file picker doesn't work on Android.
  200. make.path = make.string
  201. make.filepath = make.string
  202. else
  203. make.path = make_path
  204. make.filepath = make_path
  205. end
  206. function make.v3f(setting)
  207. return {
  208. info_text = setting.comment,
  209. setting = setting,
  210. get_formspec = function(self, avail_w)
  211. local value = vector.from_string(core.settings:get(setting.name) or setting.default)
  212. self.resettable = core.settings:has(setting.name)
  213. -- Allocate space for "Set" button
  214. avail_w = avail_w - 1
  215. local fs = "label[0,0.1;" .. get_label(setting) .. "]"
  216. local field_width = (avail_w - 3*0.25) / 3
  217. fs = fs .. ("field[%f,0.6;%f,0.8;%s;%s;%s]"):format(
  218. 0, field_width, setting.name .. "_x", "X", value.x)
  219. fs = fs .. ("field[%f,0.6;%f,0.8;%s;%s;%s]"):format(
  220. field_width + 0.25, field_width, setting.name .. "_y", "Y", value.y)
  221. fs = fs .. ("field[%f,0.6;%f,0.8;%s;%s;%s]"):format(
  222. 2 * (field_width + 0.25), field_width, setting.name .. "_z", "Z", value.z)
  223. fs = fs .. ("button[%f,0.6;1,0.8;%s;%s]"):format(avail_w, "set_" .. setting.name, fgettext("Set"))
  224. return fs, 1.4
  225. end,
  226. on_submit = function(self, fields)
  227. if fields["set_" .. setting.name] or
  228. fields.key_enter_field == setting.name .. "_x" or
  229. fields.key_enter_field == setting.name .. "_y" or
  230. fields.key_enter_field == setting.name .. "_z" then
  231. local x = tonumber(fields[setting.name .. "_x"])
  232. local y = tonumber(fields[setting.name .. "_y"])
  233. local z = tonumber(fields[setting.name .. "_z"])
  234. if is_valid_number(x) and is_valid_number(y) and is_valid_number(z) then
  235. core.settings:set(setting.name, vector.new(x, y, z):to_string())
  236. else
  237. core.log("error", "Invalid vector: " .. dump({x, y, z}))
  238. end
  239. return true
  240. end
  241. end,
  242. }
  243. end
  244. function make.flags(setting)
  245. local checkboxes = {}
  246. return {
  247. info_text = setting.comment,
  248. setting = setting,
  249. get_formspec = function(self, avail_w)
  250. local fs = {
  251. "label[0,0.1;" .. get_label(setting) .. "]",
  252. }
  253. local value = core.settings:get(setting.name) or setting.default
  254. self.resettable = core.settings:has(setting.name)
  255. checkboxes = {}
  256. for _, name in ipairs(value:split(",")) do
  257. name = name:trim()
  258. if name:sub(1, 2) == "no" then
  259. checkboxes[name:sub(3)] = false
  260. elseif name ~= "" then
  261. checkboxes[name] = true
  262. end
  263. end
  264. local columns = math.max(math.floor(avail_w / 2.5), 1)
  265. local column_width = avail_w / columns
  266. local x = 0
  267. local y = 0.55
  268. for _, possible in ipairs(setting.possible) do
  269. if possible:sub(1, 2) ~= "no" then
  270. if x >= avail_w then
  271. x = 0
  272. y = y + 0.5
  273. end
  274. local is_checked = checkboxes[possible]
  275. fs[#fs + 1] = ("checkbox[%f,%f;%s;%s;%s]"):format(
  276. x, y, setting.name .. "_" .. possible,
  277. core.formspec_escape(possible), tostring(is_checked))
  278. x = x + column_width
  279. end
  280. end
  281. return table.concat(fs, ""), y + 0.25
  282. end,
  283. on_submit = function(self, fields)
  284. local changed = false
  285. for name, _ in pairs(checkboxes) do
  286. local value = fields[setting.name .. "_" .. name]
  287. if value ~= nil then
  288. checkboxes[name] = core.is_yes(value)
  289. changed = true
  290. end
  291. end
  292. if changed then
  293. local values = {}
  294. for _, name in ipairs(setting.possible) do
  295. if name:sub(1, 2) ~= "no" then
  296. if checkboxes[name] then
  297. table.insert(values, name)
  298. else
  299. table.insert(values, "no" .. name)
  300. end
  301. end
  302. end
  303. core.settings:set(setting.name, table.concat(values, ","))
  304. end
  305. return changed
  306. end
  307. }
  308. end
  309. local function make_noise_params(setting)
  310. return {
  311. info_text = setting.comment,
  312. setting = setting,
  313. get_formspec = function(self, avail_w)
  314. -- The "defaults" noise parameter flag doesn't reset a noise
  315. -- setting to its default value, so we offer a regular reset button.
  316. self.resettable = core.settings:has(setting.name)
  317. local fs = "label[0,0.4;" .. get_label(setting) .. "]" ..
  318. ("button[%f,0;2.5,0.8;%s;%s]"):format(avail_w - 2.5, "edit_" .. setting.name, fgettext("Edit"))
  319. return fs, 0.8
  320. end,
  321. on_submit = function(self, fields, tabview)
  322. if fields["edit_" .. setting.name] then
  323. local dlg = create_change_mapgen_flags_dlg(setting)
  324. dlg:set_parent(tabview)
  325. tabview:hide()
  326. dlg:show()
  327. return true
  328. end
  329. end,
  330. }
  331. end
  332. make.noise_params_2d = make_noise_params
  333. make.noise_params_3d = make_noise_params
  334. return make