From ee791295e250418fcc9c12fba15cf0b5718b15cf Mon Sep 17 00:00:00 2001 From: Austin Hicks Date: Sun, 9 Feb 2025 19:36:33 -0800 Subject: [PATCH] WIP --- control.lua | 1 + locale/en/ui-blueprints.cfg | 26 +++++ scripts/blueprints.lua | 8 +- scripts/fa-utils.lua | 61 ++++++++++ scripts/graphics.lua | 2 +- scripts/ui/menu-items.lua | 3 +- scripts/ui/menu.lua | 30 ++--- scripts/ui/menus/blueprints-menu.lua | 167 ++++++++++++++++++++++++++- scripts/ui/sounds.lua | 4 + scripts/ui/tab-list.lua | 14 ++- 10 files changed, 290 insertions(+), 26 deletions(-) diff --git a/control.lua b/control.lua index 6c932518..16b288a7 100644 --- a/control.lua +++ b/control.lua @@ -4934,6 +4934,7 @@ script.on_event("click-hand-right", function(event) --Laterdo here: build as ghost elseif stack.is_blueprint then fa_blueprints.blueprint_menu_open(pindex) + BlueprintsMenu.blueprint_menu_tabs:open(pindex, {}) elseif stack.is_blueprint_book then fa_blueprints.blueprint_book_menu_open(pindex, false) elseif stack.is_deconstruction_item then diff --git a/locale/en/ui-blueprints.cfg b/locale/en/ui-blueprints.cfg index 7b5036df..5f32b989 100644 --- a/locale/en/ui-blueprints.cfg +++ b/locale/en/ui-blueprints.cfg @@ -1,3 +1,29 @@ [fa] ui-blueprints-menu-title=Blueprint Configuration ui-blueprints-menu-limited=Empty Blueprint with Limited Options. Import a blueprint string or select an area for more. +ui-blueprints-menu-basic=Blueprint __1__ +ui-blueprints-menu-import=Import a blueprint string to this blueprint +ui-blueprints-menu-description=Description: __1__ +ui-blueprints-menu-no-icons=No Icons +ui-blueprints-menu-icons=Icons: +ui-blueprints-menu-count-and-dims=__1__ by __2__ with __3__ entities +ui-blueprints-menu-no-components=No entities in this blueprint +ui-blueprints-menu-components-intro=Contains the following: +ui-blueprints-menu-rename=Rename this blueprint +ui-blueprints-menu-edit-desc=Edit description +ui-blueprints-menu-copy=Create a copy of this blueprint in your inventory +ui-blueprints-menu-delete=Delete this blueprint +ui-blueprints-menu-export=Export this blueprint as a string +ui-blueprints-menu-reselect=Reselect the area for this blueprint + + +ui-blueprints-import-txtbox=Paste a blueprint string in this textbox and press enter. Press escape to cancel. +ui-blueprints-rename-txtbox=Enter new name and press Enter. Escape to cancel. +ui-blueprints-description-txtbox=Enter description and press enter. Escape to cancel. +ui-blueprints-export-txtbox=Press control plus a control plus c to copy, then escape to close. + +ui-blueprints-copy-success=Copied. +ui-blueprints-copy-failed=Unable to copy. +ui-blueprints-deleted=Deleted and menu closed. + +ui-blueprints-select-first-point=Select the first point now \ No newline at end of file diff --git a/scripts/blueprints.lua b/scripts/blueprints.lua index 30be89ad..7c13398d 100644 --- a/scripts/blueprints.lua +++ b/scripts/blueprints.lua @@ -6,7 +6,6 @@ local fa_building_tools = require("scripts.building-tools") local fa_mining_tools = require("scripts.player-mining-tools") local fa_graphics = require("scripts.graphics") local dirs = defines.direction -local BlueprintsMenu = require("scripts.ui.menus.blueprints-menu") local mod = {} @@ -44,7 +43,7 @@ end function mod.get_blueprint_label(stack) local bp_data = mod.get_bp_data_for_edit(stack) local label = bp_data.blueprint.label - if label == nil then label = "" end + if label == nil then label = "no name" end return label end @@ -663,7 +662,7 @@ BLUEPRINT_MENU_LENGTH = 12 function mod.blueprint_menu_open(pindex) if players[pindex].vanilla_mode then return end - BlueprintsMenu.blueprint_menu_tabs:open(pindex, {}) + -- Opening the ui is one level up, to avoid circular imports. players[pindex].move_queue = {} @@ -678,9 +677,6 @@ function mod.blueprint_menu_open(pindex) --Play sound game.get_player(pindex).play_sound({ path = "Open-Inventory-Sound" }) - - --Load menu - mod.run_blueprint_menu(players[pindex].blueprint_menu.index, pindex, false) end function mod.blueprint_menu_close(pindex, mute_in) diff --git a/scripts/fa-utils.lua b/scripts/fa-utils.lua index fe6bb9c8..51e79d4a 100644 --- a/scripts/fa-utils.lua +++ b/scripts/fa-utils.lua @@ -1089,4 +1089,65 @@ function mod.closest_point_in_box(point, box) } end +local ALL_PROTO_KINDS = { + "item", + "font", + "map_gen_preset", + "style", + "entity", + "fluid", + "tile", + "equipment", + "damage", + "virtual_signal", + "equipment_grid", + "recipe", + "technology", + "decorative", + "particle", + "autoplace_control", + "mod_setting", + "custom_input", + "ammo_category", + "named_noise_expression", + "named_noise_function", + "item_subgroup", + "item_group", + "fuel_category", + "resource_category", + "achievement", + "module_category", + "equipment_category", + "trivial_smoke", + "shortcut", + "recipe_category", + "quality", + "surface_property", + "space_location", + "space_connection", + "custom_event", + "active_trigger", + "asteroid_chunk", + "collision_layer", + "airborne_pollutant", + "burner_usage", + "surface", + "procession", + "procession_layer_inheritance_group", +} + +-- In 2.0 all the prototypes got split up into separate attributes of +-- LuaPrototypes, but that's a userdata. This function checks them all for a +-- prototype, because in some cases we don't know what it actually is. +---@param name string +---@return LuaPrototypeBase? +function mod.find_prototype(name) + for _, f in pairs(ALL_PROTO_KINDS) do + local p = prototypes[f] + if p and p[name] then return p[name] end + end + + return nil +end + return mod diff --git a/scripts/graphics.lua b/scripts/graphics.lua index 71ce9b26..6d8e94c6 100644 --- a/scripts/graphics.lua +++ b/scripts/graphics.lua @@ -524,7 +524,7 @@ local function sprite_name(sig) fluid = "fluid", virtual = "virtual-signal", } - return typemap[sig.type] .. "." .. sig.name + return typemap[sig.type or "item"] .. "." .. sig.name end ---@param elem LuaGuiElement diff --git a/scripts/ui/menu-items.lua b/scripts/ui/menu-items.lua index cd9bbd9e..218e1e22 100644 --- a/scripts/ui/menu-items.lua +++ b/scripts/ui/menu-items.lua @@ -7,7 +7,7 @@ function mod.lazy_label(key, callback) return { key = key, label = callback, - clikc = callback, + click = callback, } end @@ -32,6 +32,7 @@ dynamic labels too (the next render gets a chance to recompute). ---@return fa.MenuItemRender function mod.clickable_label(key, label, click_handler) return { + key = key, label = function(ctx) ctx.message:fragment(label) end, diff --git a/scripts/ui/menu.lua b/scripts/ui/menu.lua index 59d5190c..f68d667c 100644 --- a/scripts/ui/menu.lua +++ b/scripts/ui/menu.lua @@ -73,11 +73,18 @@ local Sounds = require("scripts.ui.sounds") local mod = {} +--[[ +Tell the menu itself to do things from click handlers etc. +]] +---@class fa.MenuController +---@field close fun(self) + ---@class fa.MenuCtx ---@field pindex number ---@field message_builder fa.MessageBuilder ---@field state any Returned from the state function and maintained in `storage`. ---@field item_state table? Private to the menu item itself. +---@field controller fa.MenuController ---@alias fa.MenuEventCallback fun(fa.MenuCtx) ---@alias fa.MenuEventPredicate fun(fa.MenuCtx): boolean @@ -202,18 +209,7 @@ end --[[ Return a descriptor for a tab containing a menu. -The tab's name will be `menutab-menu_name`. The menu will look into -`parameters` for `parameters.menu_name`. If found, that is the menu's -parameters. For now the only parameter which can be passed is `initial_position -= { key = "foo" }`. For example: - -``` -{ - blueprint_menu = { - initial_position = { key = "read_name" }, - } -} -``` +The tab's name will be `menutab-menu_name`. Which would put the cursor in this tab on that menu item. If the tablist is only one tab, that makes it so that the menu opens to the right place directly; @@ -228,11 +224,19 @@ function mod.declare_menu(opts) ---@param ctx fa.MenuTabCtxInternal ---@return fa.MenuCtx local function build_user_ctx(ctx) + local controller = {} + ---@cast controller fa.MenuController + + function controller:close() + ctx.force_close = true + end + return { pindex = ctx.pindex, message = ctx.message, state = ctx.state.menu_state, item_state = nil, + controller = controller, } end @@ -273,7 +277,6 @@ function mod.declare_menu(opts) ---@param item fa.MenuItemRender ---@return fa.MenuCtx function build_item_ctx(ctx, item) - print(serpent.line(ctx, { nocode = true })) local u_ctx = build_user_ctx(ctx) u_ctx.item_state = ctx.state.item_states[item.key] if not u_ctx.item_state then @@ -352,6 +355,7 @@ function mod.declare_menu(opts) if not item then return end item.click(build_item_ctx(ctx, item)) + Sounds.play_menu_click(ctx.pindex) end) end diff --git a/scripts/ui/menus/blueprints-menu.lua b/scripts/ui/menus/blueprints-menu.lua index 3d920034..80973538 100644 --- a/scripts/ui/menus/blueprints-menu.lua +++ b/scripts/ui/menus/blueprints-menu.lua @@ -3,6 +3,10 @@ local MenuItems = require("scripts.ui.menu-items") local TabList = require("scripts.ui.tab-list") local Functools = require("scripts.functools") local Graphics = require("scripts.graphics") +local Blueprints = require("scripts.blueprints") +local Localising = require("scripts.localising") +local TH = require("scripts.table-helpers") +local FaUtils = require("scripts.fa-utils") local mod = {} @@ -22,8 +26,154 @@ local function render(ctx) local menu_items = {} + -- In the below note that it is long-standing mod behavior to close menus + -- after text boxes close. That's unfortunate, but we'll keep doing that for + -- now as we flesh out our gui story. + if not bp.is_blueprint_setup() then - table.insert(menu_items, MenuItems.make_label("blueprint-not-imported", { "fa.ui-blueprints-menu-limited" })) + table.insert(menu_items, MenuItems.simple_label("blueprint-info", { "fa.ui-blueprints-menu-limited" })) + else + table.insert( + menu_items, + MenuItems.simple_label("blueprint-info", { "fa.ui-blueprints-menu-basic", Blueprints.get_blueprint_label(bp) }) + ) + end + + -- Blueprints which are empty can't have descriptions; don't show it. + if bp.is_blueprint_setup then + table.insert( + menu_items, + MenuItems.lazy_label("description", function(ctx) + ctx.message:fragment({ "fa.ui-blueprints-menu-description", Blueprints.get_blueprint_description(bp) }) + end) + ) + + table.insert( + menu_items, + MenuItems.lazy_label("icons", function(ctx) + local icons = bp.preview_icons + if not icons or not next(icons) then + ctx.message:fragment({ "fa.ui-blueprints-menu-no-icons" }) + return + end + + table.sort(icons, function(a, b) + return a.index < b.index + end) + + local icons = TH.map(bp.preview_icons, function(icon) + -- Localising this is tricky because it could be from anywhere. + + local proto = FaUtils.find_prototype(icon.signal.name) + if proto then return Localising.get_localised_name_with_fallback(proto) end + + return icon.signal.name + end) + + ctx.message:fragment({ "fa.ui-blueprints-menu-icons" }) + for _, i in ipairs(icons) do + ctx.message:list_item(i) + end + end) + ) + + table.insert( + menu_items, + MenuItems.lazy_label("count-and-dims", function(ctx) + local count = bp.get_blueprint_entity_count() + local width, height = Blueprints.get_blueprint_width_and_height(pindex) + ctx.message:fragment({ "fa.ui-blueprints-menu-count-and-dims", width, height, count }) + end) + ) + + table.insert( + menu_items, + MenuItems.lazy_label("components", function(ctx) + local comps = bp.get_blueprint_entities() + if not comps or not next(comps) then + ctx.message:fragment({ "fa.ui-blueprints-menu-no-components" }) + return + end + + ctx.message:fragment({ "fa.ui-blueprints-menu-components-intro" }) + local counts = {} + for _, comp in ipairs(comps) do + counts[comp.name] = (counts[comp.name] or 0) + 1 + end + + local by_name = TH.set_to_sorted_array(counts) + print(serpent.line(by_name)) + table.sort(by_name, function(a, b) + return a[2] > b[2] + end) + + for _, ent in pairs(by_name) do + local comp, count = ent[1], ent[2] + ctx.message:fragment(Localising.get_localised_name_with_fallback(prototypes.entity[comp])) + end + end) + ) + + table.insert( + menu_items, + MenuItems.clickable_label("rename", { "fa.ui-blueprints-menu-rename" }, function(ctx) + storage.players[ctx.pindex].blueprint_menu.edit_label = true + Graphics.create_text_field_frame(ctx.pindex, "blueprint-edit-label") + ctx.message:fragment({ "fa.ui-blueprints-rename-txtbox" }) + ctx.controller:close() + end) + ) + + table.insert( + menu_items, + MenuItems.clickable_label("edit-desc", { "fa.ui-blueprints-menu-edit-desc" }, function(ctx) + storage.players[ctx.pindex].blueprint_menu.edit_description = true + Graphics.create_text_field_frame(ctx.pindex, "blueprint-edit-description") + ctx.message:fragment({ "fa.ui-blueprints-description-txtbox" }) + ctx.controller:close() + end) + ) + + table.insert( + menu_items, + MenuItems.clickable_label("copy", { "fa.ui-blueprints-menu-copy" }, function(ctx) + local p = game.get_player(ctx.pindex) + if not p then return end + -- The deepcopy here shouldn't be necessary, indeed it shouldn't do + -- anything at all. But this is copied over from old code, and it's + -- not worth determining if it's secretly needed for some weird + -- reason. + if p.insert(table.deepcopy(bp)) > 0 then + ctx.message:fragment({ "fa.ui-blueprints-copy-success" }) + else + ctx.message:fragment({ "fa.ui-blueprints-copy-failed" }) + end + end) + ) + + table.insert( + menu_items, + MenuItems.clickable_label("delete", { "fa.ui-blueprints-menu-delete" }, function(ctx) + local p = game.get_player(ctx.pindex) + if not p then return end + + bp.set_stack({ name = "blueprint", count = 1 }) + bp.set_stack(nil) --calls event handler to delete empty planners. ctx.controller:close() + + ctx.message:fragment({ "fa.blueprints-ui-deleted" }) + ctx.controller:close() + end) + ) + + table.insert( + menu_items, + MenuItems.clickable_label("export", { "fa.ui-blueprints-menu-export" }, function(ctx) + storage.players[ctx.pindex].blueprint_menu.edit_export = true + Graphics.create_text_field_frame(ctx.pindex, "blueprint-edit-export", bp.export_stack()) + ctx.message:fragment({ "fa.ui-blueprints-export-txtbox" }) + ctx.controller:close() + end) + ) end table.insert( @@ -31,8 +181,19 @@ local function render(ctx) MenuItems.clickable_label("import", { "fa.ui-blueprints-menu-import" }, function(ctx) -- For now, this shells out to legacy UI: close ourselves, and let it -- do the work. - state.players[pindex].blueprint_menu.edit_import = true - local frame = Graphics.create_text_field_frame(pindex, "blueprint-edit-import") + storage.players[pindex].blueprint_menu.edit_import = true + Graphics.create_text_field_frame(pindex, "blueprint-edit-import") + ctx.message:fragment({ "fa.ui-blueprints-import-txtbox" }) + ctx.controller:close() + end) + ) + + table.insert( + menu_items, + MenuItems.clickable_label("reselect", { "fa.ui-blueprints-menu-reselect" }, function(ctx) + storage.players[ctx.pindex].blueprint_reselecting = true + ctx.message:fragment({ "fa.ui-blueprints-select-first-point" }) + ctx.controller:close() end) ) diff --git a/scripts/ui/sounds.lua b/scripts/ui/sounds.lua index f13be160..b4437968 100644 --- a/scripts/ui/sounds.lua +++ b/scripts/ui/sounds.lua @@ -15,6 +15,10 @@ function mod.play_menu_move(pindex) game.get_player(pindex).play_sound({ path = "Inventory-Move" }) end +function mod.play_menu_click(pindex) + game.get_player(pindex).play_sound({ path = "utility/inventory_click" }) +end + function mod.play_menu_wrap(pindex) game.get_player(pindex).play_sound({ path = "inventory-wrap-around" }) end diff --git a/scripts/ui/tab-list.lua b/scripts/ui/tab-list.lua index ea69c027..c7e64155 100644 --- a/scripts/ui/tab-list.lua +++ b/scripts/ui/tab-list.lua @@ -157,6 +157,8 @@ TabList.on_down = build_simple_method("on_down") TabList.on_left = build_simple_method("on_left") ---@type fun(self, number) TabList.on_right = build_simple_method("on_right") +---@type fun(self, number) +TabList.on_click = build_simple_method("on_click") -- Perform the flow for focusing a tab. Does this unconditionally, so be careful -- not to over-call it. @@ -236,8 +238,16 @@ end ---@param force_reset boolean? If true, also dump state. function TabList:close(pindex, force_reset) - for i = 1, #self.tab_order do - self:_do_callback(pindex, i, "on_tab_list_closed") + -- Our lame event handling story where more than one event handler can get + -- called for the same event combined with the new GUI framework still being + -- WIP means that double-close is apparently possible. We already know we're + -- going to fix that, so for now just guard against it. + if not tablist_storage[pindex] or not tablist_storage[pindex][self.menu_name] then return end + + if tablist_storage[pindex][self.menu_name].currently_open then + for i = 1, #self.tab_order do + self:_do_callback(pindex, i, "on_tab_list_closed") + end end tablist_storage[pindex][self.menu_name].currently_open = false