Implement new API for adding custom footer-buttons

This allows mods to place their own buttons
alongside the trash, sort, and settings buttons in
the inventory’s footer. The API mimics the custom
tab API.
This commit is contained in:
Jaidyn Ann 2024-01-11 22:21:41 -06:00
parent 74f8fe64a8
commit 623b14bfe8
4 changed files with 292 additions and 187 deletions

View File

@ -20,7 +20,7 @@ local function lf(path)
end end
i3 = { i3 = {
version = 1161, version = 1162,
data = core.deserialize(storage:get_string"data") or {}, data = core.deserialize(storage:get_string"data") or {},
settings = { settings = {
@ -102,6 +102,7 @@ i3 = {
plants = {}, plants = {},
modules = {}, modules = {},
minitabs = {}, minitabs = {},
footer_buttons = {},
craft_types = {}, craft_types = {},
recipe_filters = {}, recipe_filters = {},

View File

@ -1,5 +1,6 @@
local http = ... local http = ...
local make_fs, get_inventory_fs = i3.files.gui() local make_fs, get_inventory_fs, confirm_trash_footer_fs, settings_footer_fs = i3.files.gui()
local home_footer_fields, confirm_trash_footer_fields, settings_footer_fields = i3.files.fields()
IMPORT("sorter", "sort_inventory", "play_sound") IMPORT("sorter", "sort_inventory", "play_sound")
IMPORT("sort", "concat", "copy", "insert", "remove") IMPORT("sort", "concat", "copy", "insert", "remove")
@ -257,6 +258,88 @@ function i3.override_tab(name, newdef)
end end
end end
function i3.new_footer_button(name, def)
if not true_str(name) then
return err "i3.new_footer_button: button name missing"
elseif not true_table(def) then
return err "i3.new_footer_button: button definition missing"
elseif not true_str(def.description) then
return err "i3.new_footer_button: description missing"
elseif not def.image then
return err "i3.new_footer_button: image missing"
elseif #i3.footer_buttons == 16 then
return err(fmt("i3.new_footer_button: cannot add '%s' button. Limit reached (16).", def.name))
end
def.name = name
insert(i3.footer_buttons, def)
end
function i3.remove_footer_button(name)
if not true_str(name) then
return err "i3.remove_footer_button: tab name missing"
end
for i = #i3.footer_buttons, 2, -1 do
local def = i3.footer_buttons[i]
if def and name == def.name then
remove(i3.footer_buttons, i)
end
end
end
function i3.override_footer_button(name, newdef)
if not true_str(name) then
return err "i3.override_footer_button: button name missing"
elseif not true_table(newdef) then
return err "i3.override_footer_button: button definition missing"
elseif not true_str(newdef.description) then
return err "i3.override_footer_button: description missing"
elseif not true_str(newdef.image) then
return err "i3.override_footer_button: image missing"
end
newdef.name = name
for i, def in ipairs(i3.footer_buttons) do
if def.name == name then
i3.footer_buttons[i] = newdef
break
end
end
end
i3.new_footer_button("trash", {
description = S"Clear inventory",
image = "i3_trash.png",
formspec = confirm_trash_footer_fs,
fields = confirm_trash_footer_fields,
})
i3.new_footer_button("sort", {
description = S"Sort inventory",
image = "i3_sort.png",
fields = function(player, data, fields)
sort_inventory(player, data)
-- Immediately disable, since nothing is displayed.
return false
end,
})
i3.new_footer_button("settings", {
description = S"Settings",
image = "i3_settings.png",
formspec = settings_footer_fs,
fields = settings_footer_fields,
})
i3.new_footer_button("home", {
description = S"Go home",
image = "i3_home.png",
fields = home_footer_fields,
})
i3.register_craft_type("digging", { i3.register_craft_type("digging", {
description = S"Digging", description = S"Digging",
icon = "i3_steelpick.png", icon = "i3_steelpick.png",

View File

@ -9,6 +9,7 @@ IMPORT("valid_item", "get_stack", "craft_stack", "clean_name", "check_privs", "s
IMPORT("msg", "is_fav", "pos_to_str", "str_to_pos", "add_hud_waypoint", "play_sound", "reset_data") IMPORT("msg", "is_fav", "pos_to_str", "str_to_pos", "add_hud_waypoint", "play_sound", "reset_data")
IMPORT("search", "sort_inventory", "sort_by_category", "get_recipes", "get_detached_inv", "update_inv_size") IMPORT("search", "sort_inventory", "sort_by_category", "get_recipes", "get_detached_inv", "update_inv_size")
-- Fields-handler for any tab displaying the inventory & footer (that is, with def.slots true).
local function inv_fields(player, data, fields) local function inv_fields(player, data, fields)
local name = data.player_name local name = data.player_name
local inv = player:get_inventory() local inv = player:get_inventory()
@ -19,6 +20,13 @@ local function inv_fields(player, data, fields)
data.font_size = tonumber(fields.sb_font_size:match"-?%d+$") data.font_size = tonumber(fields.sb_font_size:match"-?%d+$")
end end
local footer = data.footer_button and i3.footer_buttons[data.footer_button]
if footer and footer.fields then
if not footer.fields(player, data, fields) then
data.footer_button = nil
end
end
for field in pairs(fields) do for field in pairs(fields) do
if sub(field, 1, 4) == "btn_" then if sub(field, 1, 4) == "btn_" then
data.subcat = indexof(i3.categories, sub(field, 5)) data.subcat = indexof(i3.categories, sub(field, 5))
@ -93,8 +101,7 @@ local function inv_fields(player, data, fields)
end end
if fields.quit then if fields.quit then
data.confirm_trash = nil data.footer_button = nil
data.show_settings = nil
data.waypoint_see = nil data.waypoint_see = nil
data.bag_rename = nil data.bag_rename = nil
data.goto_page = nil data.goto_page = nil
@ -103,58 +110,9 @@ local function inv_fields(player, data, fields)
data.enable_search = nil data.enable_search = nil
end end
elseif fields.trash then
data.show_settings = nil
data.confirm_trash = true
elseif fields.settings then
if not data.show_settings then
data.confirm_trash = nil
data.show_settings = true
else
data.show_settings = nil
end
elseif fields.confirm_trash_yes or fields.confirm_trash_no then
if fields.confirm_trash_yes then
inv:set_list("main", {})
inv:set_list("craft", {})
end
data.confirm_trash = nil
elseif fields.close_settings then
data.show_settings = nil
elseif fields.close_preview then elseif fields.close_preview then
data.waypoint_see = nil data.waypoint_see = nil
elseif fields.sort then
sort_inventory(player, data)
elseif (fields.home or fields.set_home) and not check_privs(name, {home = true}) then
return msg(name, "'home' privilege missing")
elseif fields.home then
if sethome then
if not sethome.go(name) then
return msg(name, "No home set")
end
elseif not data.home then
return msg(name, "No home set")
else
safe_teleport(player, str_to_pos(data.home))
end
msg(name, S"Welcome back home!")
elseif fields.set_home then
if sethome then
sethome.set(name, player:get_pos())
else
data.home = pos_to_str(player:get_pos(), 1)
end
elseif fields.bag_rename then elseif fields.bag_rename then
data.bag_rename = true data.bag_rename = true
@ -429,6 +387,55 @@ local function rcp_fields(player, data, fields)
end end
end end
-- Fields input-handler for the Home footer-button.
local function home_footer_fields(player, data, fields)
local name = player:get_player_name()
-- Use minetest_games sethome API, if available.
if sethome then
if not sethome.go(name) then
msg(name, "No home set")
else
msg(name, S"Welcome back home!")
end
-- Otherwise, use i3s home.
elseif not data.home then
msg(name, "No home set")
elseif name then
safe_teleport(player, str_to_pos(data.home))
msg(name, S"Welcome back home!")
end
-- Immediately disable this footer-dialogue, since nothing is displayed.
return false
end
-- Fields input-handler for the “Confirm trash” footer-dialogue.
local function confirm_trash_footer_fields(player, data, fields)
local inv = player:get_inventory()
if fields.confirm_trash_yes then
inv:set_list("main", {})
inv:set_list("craft", {})
end
-- In any case, disable this footer-dialogue.
return not (fields.confirm_trash_yes or fields.confirm_trash_no)
end
-- Fields input-handler for the settings footer-dialogue.
local function settings_footer_fields(player, data, fields)
if fields.set_home and not check_privs(name, {home = true}) then
return msg(name, "'home' privilege missing")
elseif fields.set_home then
if sethome then
sethome.set(player:get_player_name(), player:get_pos())
else
data.home = pos_to_str(player:get_pos(), 1)
end
end
return not fields.close_settings
end
core.register_on_player_receive_fields(function(player, formname, fields) core.register_on_player_receive_fields(function(player, formname, fields)
local name = player:get_player_name() local name = player:get_player_name()
@ -465,6 +472,14 @@ core.register_on_player_receive_fields(function(player, formname, fields)
data.itab = tonumber(f:sub(-1)) data.itab = tonumber(f:sub(-1))
sort_by_category(data) sort_by_category(data)
break break
elseif sub(f, 1, 7) == "footer_" then
local footer_name = sub(f, 8)
for i, footer in ipairs(i3.footer_buttons) do
if footer.name == footer_name then
data.footer_button = i
break
end
end
end end
end end
@ -484,3 +499,5 @@ core.register_on_player_receive_fields(function(player, formname, fields)
return true, set_fs(player) return true, set_fs(player)
end) end)
return home_footer_fields, confirm_trash_footer_fields, settings_footer_fields

View File

@ -569,144 +569,148 @@ local function get_container(fs, data, player, yoffset, ctn_len, award_list, awa
end end
end end
local function show_settings(fs, data, player) -- Get the formspec of the footer “Confirm trash” dialogue.
if data.confirm_trash then local function confirm_trash_footer_fs(player, data, fs)
image(2.8, 10.65, 4.6, 0.7, PNG.bg_goto) image(2.8, 10.65, 4.6, 0.7, PNG.bg_goto)
label(3.02, 11, "Confirm trash?") label(3.02, 11, "Confirm trash?")
image_button(5.07, 10.75, 1, 0.5, "", "confirm_trash_yes", "Yes") image_button(5.07, 10.75, 1, 0.5, "", "confirm_trash_yes", "Yes")
image_button(6.17, 10.75, 1, 0.5, "", "confirm_trash_no", "No") image_button(6.17, 10.75, 1, 0.5, "", "confirm_trash_no", "No")
elseif data.show_settings then
fs"container[-0.06,0]"
image(2.2, 9, 6.1, 2.35, PNG.bg_content)
local show_home = data.show_setting == "home"
local show_style = data.show_setting == "style"
local show_sorting = data.show_setting == "sorting"
fs"style[setting_home,setting_style,setting_sorting;font=bold;font_size=16;sound=i3_click]"
fs("style[setting_home:hovered;textcolor=%s]", show_home and colors.yellow or "#fff")
fs("style[setting_style:hovered;textcolor=%s]", show_style and colors.yellow or "#fff")
fs("style[setting_sorting:hovered;textcolor=%s]", show_sorting and colors.yellow or "#fff")
fs("style[setting_home;bgimg=%s;bgimg_hovered=%s;bgimg_middle=9;padding=-9;textcolor=%s]",
show_home and PNG.pagenum_hover or "", PNG.pagenum_hover,
show_home and colors.yellow or "#ddd")
fs("style[setting_style;bgimg=%s;bgimg_hovered=%s;bgimg_middle=9;padding=-9;textcolor=%s]",
show_style and PNG.pagenum_hover or "", PNG.pagenum_hover,
show_style and colors.yellow or "#ddd")
fs("style[setting_sorting;bgimg=%s;bgimg_hovered=%s;bgimg_middle=9;padding=-9;textcolor=%s]",
show_sorting and PNG.pagenum_hover or "", PNG.pagenum_hover,
show_sorting and colors.yellow or "#ddd")
local X = 2.5
button(X, 9.1, 1.6, 0.55, "setting_home", "Home")
button(X + 1.7, 9.1, 1.6, 0.55, "setting_style", "Style")
button(X + 3.4, 9.1, 1.6, 0.55, "setting_sorting", "Sorting")
image_button(X + 5.12, 9.2, 0.25, 0.25, PNG.cancel_hover .. "^\\[brighten", "close_settings", "")
if show_home then
local coords, c, str = {"X", "Y", "Z"}, 0, ES"No home set"
local home = data.home
if sethome then
-- i3 stores home coordinates with one decimal of precision, as the blow fmt() statement
-- assumes. So we need to trim sethomes coordinates to one decimal, likewise.
local home_pos = sethome.get(player:get_player_name())
home = string.format("(%.1f,%.1f,%.1f)", home_pos.x, home_pos.y, home_pos.z)
end
if home then
str = home:gsub(",", " "):sub(2,-2):gsub("%.%d", ""):gsub(
"(%-?%d+)", function(a)
c++
return fmt("<b>%s: <style color=%s font=mono>%s</style></b>",
coords[c], colors.blue, a)
end)
end
hypertext(2.2, 9.9, 6, 0.6, "home_pos", fmt("<global size=16><center>%s</center>", str))
fs("style[set_home;padding=20,10,-210,-10;fgimg=%s;fgimg_hovered=%s]", PNG.home_px, PNG.home_px_hover)
image_button(4.1, 10.4, 2.2, 0.7, "", "set_home", "")
label(4.9, 10.75, "Set home")
elseif show_style then
checkbox(2.6, 9.95, "cb_hide_tabs", "Hide tabs", tostring(data.hide_tabs))
checkbox(2.6, 10.4, "cb_legacy_inventory", "Legacy inventory", tostring(data.legacy_inventory))
checkbox(2.6, 10.85, "cb_wielditem_hud", "HUD description", tostring(data.wielditem_hud))
if not recipe_filter_set() then
checkbox(5.3, 10.85, "cb_collapse", "Collapse list", tostring(data.collapse))
end
local sign = (data.font_size > 0 and "+") or (data.font_size > 0 and "-") or ""
label(5.3, 9.95, ES"Font size" .. fmt(": %s", sign .. data.font_size))
local range = 8
fs("scrollbaroptions[min=-%u;max=%u;smallstep=1;largestep=1;thumbsize=2]", range, range)
fs("scrollbar[5.3,10.2;2.55,0.3;horizontal;sb_font_size;%d+]", data.font_size)
fs("tooltip[cb_hide_tabs;%s;#32333899;#fff]",
ES"Enable this option to change the style of the right panel")
fs("tooltip[cb_legacy_inventory;%s;#32333899;#fff]",
ES"Enable this option to set the classic inventory size in Minetest")
fs("tooltip[cb_wielditem_hud;%s;#32333899;#fff]",
ES"Enable this option to show the wielded item description in your HUD")
fs("tooltip[cb_collapse;%s;#32333899;#fff]",
ES"Enable this option to collapse the inventory list by grouping some items")
elseif show_sorting then
checkbox(2.6, 9.95, "cb_inv_compress", "Compression", tostring(data.inv_compress))
checkbox(2.6, 10.4, "cb_reverse_sorting", "Reverse mode", tostring(data.reverse_sorting))
checkbox(2.6, 10.85, "cb_ignore_hotbar", "Ignore hotbar", tostring(data.ignore_hotbar))
checkbox(5.3, 9.95, "cb_auto_sorting", "Automation", tostring(data.auto_sorting))
local methods = {}
for _, v in ipairs(i3.sorting_methods) do
local name = toupper(v.name)
insert(methods, name)
end
label(5.3, 10.4, ES"Sorting method:")
fs("dropdown[%f,%f;2.6,0.5;dd_sorting_method;%s;%u;true]", 5.3, 10.6, concat(methods, ","), data.sort)
local desc = i3.sorting_methods[data.sort].description
if desc then
tooltip(5.3, 10.6, 2.4, 0.5, ESC(desc))
end
fs("tooltip[cb_inv_compress;%s;#32333899;#fff]",
ES"Enable this option to compress your inventory")
fs("tooltip[cb_reverse_sorting;%s;#32333899;#fff]",
ES"Enable this option to sort your inventory in reverse order")
fs("tooltip[cb_ignore_hotbar;%s;#32333899;#fff]",
ES"Enable this option to sort your inventory except the hotbar slots")
fs("tooltip[cb_auto_sorting;%s;#32333899;#fff]",
ES"Enable this option to sort your inventory automatically")
end
fs"container_end[]"
end
end end
local function get_footer(fs, data, player) -- Get the formspec of the footer settings-dialogue.
local btn = { local function settings_footer_fs(player, data, fs)
{"trash", ES"Clear inventory"}, fs"container[-0.06,0]"
{"sort", ES"Sort inventory"}, image(2.2, 9, 6.1, 2.35, PNG.bg_content)
{"settings", ES"Settings"},
{"home", ES"Go home"},
}
for i, v in ipairs(btn) do local show_home = data.show_setting == "home"
local btn_name, tooltip = unpack(v) local show_style = data.show_setting == "style"
fs("style[%s;fgimg=%s;fgimg_hovered=%s;content_offset=0]", local show_sorting = data.show_setting == "sorting"
btn_name, PNG[btn_name], PNG[fmt("%s_hover", btn_name)])
image_button(i + 3.43 - (i * 0.4), 11.43, 0.35, 0.35, "", btn_name, "") fs"style[setting_home,setting_style,setting_sorting;font=bold;font_size=16;sound=i3_click]"
fs("tooltip[%s;%s;#32333899;#fff]", btn_name, tooltip) fs("style[setting_home:hovered;textcolor=%s]", show_home and colors.yellow or "#fff")
fs("style[setting_style:hovered;textcolor=%s]", show_style and colors.yellow or "#fff")
fs("style[setting_sorting:hovered;textcolor=%s]", show_sorting and colors.yellow or "#fff")
fs("style[setting_home;bgimg=%s;bgimg_hovered=%s;bgimg_middle=9;padding=-9;textcolor=%s]",
show_home and PNG.pagenum_hover or "", PNG.pagenum_hover,
show_home and colors.yellow or "#ddd")
fs("style[setting_style;bgimg=%s;bgimg_hovered=%s;bgimg_middle=9;padding=-9;textcolor=%s]",
show_style and PNG.pagenum_hover or "", PNG.pagenum_hover,
show_style and colors.yellow or "#ddd")
fs("style[setting_sorting;bgimg=%s;bgimg_hovered=%s;bgimg_middle=9;padding=-9;textcolor=%s]",
show_sorting and PNG.pagenum_hover or "", PNG.pagenum_hover,
show_sorting and colors.yellow or "#ddd")
local X = 2.5
button(X, 9.1, 1.6, 0.55, "setting_home", "Home")
button(X + 1.7, 9.1, 1.6, 0.55, "setting_style", "Style")
button(X + 3.4, 9.1, 1.6, 0.55, "setting_sorting", "Sorting")
image_button(X + 5.12, 9.2, 0.25, 0.25, PNG.cancel_hover .. "^\\[brighten", "close_settings", "")
if show_home then
local coords, c, str = {"X", "Y", "Z"}, 0, ES"No home set"
local home = data.home
if sethome then
-- i3 stores home coordinates with one decimal of precision, as the blow fmt() statement
-- assumes. So we need to trim sethomes coordinates to one decimal, likewise.
local home_pos = sethome.get(player:get_player_name())
home = string.format("(%.1f,%.1f,%.1f)", home_pos.x, home_pos.y, home_pos.z)
end
if home then
str = home:gsub(",", " "):sub(2,-2):gsub("%.%d", ""):gsub(
"(%-?%d+)", function(a)
c++
return fmt("<b>%s: <style color=%s font=mono>%s</style></b>",
coords[c], colors.blue, a)
end)
end
hypertext(2.2, 9.9, 6, 0.6, "home_pos", fmt("<global size=16><center>%s</center>", str))
fs("style[set_home;padding=20,10,-210,-10;fgimg=%s;fgimg_hovered=%s]", PNG.home_px, PNG.home_px_hover)
image_button(4.1, 10.4, 2.2, 0.7, "", "set_home", "")
label(4.9, 10.75, "Set home")
elseif show_style then
checkbox(2.6, 9.95, "cb_hide_tabs", "Hide tabs", tostring(data.hide_tabs))
checkbox(2.6, 10.4, "cb_legacy_inventory", "Legacy inventory", tostring(data.legacy_inventory))
checkbox(2.6, 10.85, "cb_wielditem_hud", "HUD description", tostring(data.wielditem_hud))
if not recipe_filter_set() then
checkbox(5.3, 10.85, "cb_collapse", "Collapse list", tostring(data.collapse))
end
local sign = (data.font_size > 0 and "+") or (data.font_size > 0 and "-") or ""
label(5.3, 9.95, ES"Font size" .. fmt(": %s", sign .. data.font_size))
local range = 8
fs("scrollbaroptions[min=-%u;max=%u;smallstep=1;largestep=1;thumbsize=2]", range, range)
fs("scrollbar[5.3,10.2;2.55,0.3;horizontal;sb_font_size;%d+]", data.font_size)
fs("tooltip[cb_hide_tabs;%s;#32333899;#fff]",
ES"Enable this option to change the style of the right panel")
fs("tooltip[cb_legacy_inventory;%s;#32333899;#fff]",
ES"Enable this option to set the classic inventory size in Minetest")
fs("tooltip[cb_wielditem_hud;%s;#32333899;#fff]",
ES"Enable this option to show the wielded item description in your HUD")
fs("tooltip[cb_collapse;%s;#32333899;#fff]",
ES"Enable this option to collapse the inventory list by grouping some items")
elseif show_sorting then
checkbox(2.6, 9.95, "cb_inv_compress", "Compression", tostring(data.inv_compress))
checkbox(2.6, 10.4, "cb_reverse_sorting", "Reverse mode", tostring(data.reverse_sorting))
checkbox(2.6, 10.85, "cb_ignore_hotbar", "Ignore hotbar", tostring(data.ignore_hotbar))
checkbox(5.3, 9.95, "cb_auto_sorting", "Automation", tostring(data.auto_sorting))
local methods = {}
for _, v in ipairs(i3.sorting_methods) do
local name = toupper(v.name)
insert(methods, name)
end
label(5.3, 10.4, ES"Sorting method:")
fs("dropdown[%f,%f;2.6,0.5;dd_sorting_method;%s;%u;true]", 5.3, 10.6, concat(methods, ","), data.sort)
local desc = i3.sorting_methods[data.sort].description
if desc then
tooltip(5.3, 10.6, 2.4, 0.5, ESC(desc))
end
fs("tooltip[cb_inv_compress;%s;#32333899;#fff]",
ES"Enable this option to compress your inventory")
fs("tooltip[cb_reverse_sorting;%s;#32333899;#fff]",
ES"Enable this option to sort your inventory in reverse order")
fs("tooltip[cb_ignore_hotbar;%s;#32333899;#fff]",
ES"Enable this option to sort your inventory except the hotbar slots")
fs("tooltip[cb_auto_sorting;%s;#32333899;#fff]",
ES"Enable this option to sort your inventory automatically")
end
fs"container_end[]"
end
-- Get the inventory-footers formspec.
local function get_footer(fs, data, player)
-- Render individual footer buttons.
local starting_x = 3.43 - (.3 * (#i3.footer_buttons - 4)) -- Center the icons
for i, btn in ipairs(i3.footer_buttons) do
local btn_name = "footer_" .. btn.name
fs("style[%s;fgimg=%s;fgimg_hovered=%s^\\[brighten^\\[colorize:#fff:100;content_offset=0]",
btn_name, btn.image, btn.image)
image_button(i + starting_x - (i * 0.4), 11.43, 0.35, 0.35, "", btn_name, "")
fs("tooltip[%s;%s;#32333899;#fff]", btn_name, btn.description)
end
-- Render the current-selected buttons formspec, if one is active.
footer = i3.footer_buttons[data.footer_button]
if footer then
if footer.formspec then
footer.formspec(player, data, fs)
end
end end
show_settings(fs, data, player)
end end
local function get_slots(fs, data, player) local function get_slots(fs, data, player)
@ -1821,4 +1825,4 @@ local function make_fs(player, data)
return fs return fs
end end
return make_fs, get_inventory_fs return make_fs, get_inventory_fs, confirm_trash_footer_fs, settings_footer_fs