Первый фикс

Пачки некоторых позиций увеличены
This commit is contained in:
2024-03-01 20:53:32 +03:00
commit 7c9c708c92
23653 changed files with 767936 additions and 0 deletions

View File

@@ -0,0 +1,665 @@
require("ui.base.view_state")
-- The main GUI parts for the compact dialog
-- ** LOCAL UTIL **
local function determine_available_columns(lines, frame_width)
local frame_border_size = 12
local table_padding, table_spacing = 8, 12
local recipe_and_check_width = 58
local button_width, button_spacing = 36, 4
local max_module_count = 0
for _, line in pairs(lines) do
if line.subfloor == nil then
local module_kinds = ModuleSet.get_module_kind_amount(line.machine.module_set)
max_module_count = math.max(max_module_count, module_kinds)
end
if line.beacon ~= nil then
local module_kinds = ModuleSet.get_module_kind_amount(line.beacon.module_set)
max_module_count = math.max(max_module_count, module_kinds)
end
end
local used_width = 0
used_width = used_width + (frame_border_size * 2) -- border on both sides
used_width = used_width + (table_padding * 2) -- padding on both sides
used_width = used_width + (table_spacing * 4) -- 5 columns -> 4 spaces
used_width = used_width + recipe_and_check_width -- constant
-- Add up machines button, module buttons, and spacing for them
used_width = used_width + button_width + (max_module_count * button_width) + (max_module_count * button_spacing)
-- Calculate the remaining width and divide by the amount a button takes up
local available_columns = (frame_width - used_width + button_spacing) / (button_width + button_spacing)
return math.floor(available_columns) -- amount is floored as to not cause a horizontal scrollbar
end
local function determine_table_height(lines, column_counts)
local total_height = 0
for _, line in pairs(lines) do
local items_height = 0
for column, count in pairs(column_counts) do
local column_height = math.ceil(line[column].count / count)
items_height = math.max(items_height, column_height)
end
local machines_height = (line.beacon ~= nil) and 2 or 1
total_height = total_height + math.max(machines_height, items_height)
end
return total_height
end
local function determine_column_counts(lines, available_columns)
local column_counts = {Ingredient = 1, Product = 1, Byproduct = 0} -- ordered by priority
available_columns = available_columns - 2 -- two buttons are already assigned
local previous_height, increment = math.huge, 1
while available_columns > 0 do
local table_heights, minimal_height = {}, math.huge
for column, count in pairs(column_counts) do
local potential_column_counts = fancytable.shallow_copy(column_counts)
potential_column_counts[column] = count + increment
local new_height = determine_table_height(lines, potential_column_counts)
table_heights[column] = new_height
minimal_height = math.min(minimal_height, new_height)
end
-- If increasing any column by 1 doesn't change the height, try incrementing by more
-- until height is decreased, or no columns are available anymore
if not (minimal_height < previous_height) and increment < available_columns then
increment = increment + 1
else
for column, height in pairs(table_heights) do
if available_columns > 0 and height == minimal_height then
column_counts[column] = column_counts[column] + 1
available_columns = available_columns - 1
break
end
end
previous_height, increment = minimal_height, 1 -- reset these
end
end
return column_counts
end
local function add_checkmark_button(parent_flow, line, relevant_line)
parent_flow.add{type="checkbox", state=relevant_line.done, mouse_button_filter={"left"},
tags={mod="fp", on_gui_checked_state_changed="checkmark_compact_line", line_id=line.id}}
end
local function add_recipe_button(parent_flow, line, relevant_line, metadata)
local recipe_proto = relevant_line.recipe.proto
local style = (line.subfloor ~= nil) and "flib_slot_button_blue_small" or "flib_slot_button_default_small"
style = (relevant_line.done) and "flib_slot_button_grayscale_small" or style
local tooltip = (line.subfloor == nil) and {"fp.tt_title", recipe_proto.localised_name}
or {"", {"fp.tt_title", recipe_proto.localised_name}, metadata.recipe_tutorial_tt}
parent_flow.add{type="sprite-button", tags={mod="fp", on_gui_click="act_on_compact_recipe", line_id=line.id},
sprite=recipe_proto.sprite, tooltip=tooltip, style=style, mouse_button_filter={"left-and-right"}}
end
local function add_modules_flow(parent_flow, parent_type, line, metadata)
for _, module in ipairs(ModuleSet.get_in_order(line[parent_type].module_set)) do
local number_line = {"", "\n", module.amount, " ", {"fp.pl_module", module.amount}}
local tooltip = {"", {"fp.tt_title", module.proto.localised_name}, number_line, metadata.module_tutorial_tt}
local style = (line.done) and "flib_slot_button_grayscale_small" or "flib_slot_button_default_small"
parent_flow.add{type="sprite-button", sprite=module.proto.sprite, tooltip=tooltip,
tags={mod="fp", on_gui_click="act_on_compact_module", line_id=line.id, module_id=module.id,
parent_type=parent_type}, number=module.amount, style=style, mouse_button_filter={"left-and-right"}}
end
end
local function add_machine_flow(parent_flow, line, metadata)
if line.subfloor == nil then
local machine_flow = parent_flow.add{type="flow", direction="horizontal"}
local machine_proto = line.machine.proto
local count, tooltip_line = util.format.machine_count(line.machine.count, (line.production_ratio > 0), true)
local tooltip = {"", {"fp.tt_title", machine_proto.localised_name}, "\n", tooltip_line,
metadata.machine_tutorial_tt}
local style = (line.done) and "flib_slot_button_grayscale_small" or "flib_slot_button_default_small"
machine_flow.add{type="sprite-button", sprite=machine_proto.sprite, number=count,
tooltip=tooltip, tags={mod="fp", on_gui_click="act_on_compact_machine", type="machine", line_id=line.id},
style=style, mouse_button_filter={"left-and-right"}}
add_modules_flow(machine_flow, "machine", line, metadata)
end
end
local function add_beacon_flow(parent_flow, line, metadata)
if line.subfloor == nil and line.beacon ~= nil then
local beacon_flow = parent_flow.add{type="flow", direction="horizontal"}
local beacon_proto = line.beacon.proto
local plural_parameter = (line.beacon.amount == 1) and 1 or 2 -- needed because the amount can be decimal
local number_line = {"", "\n", line.beacon.amount, " ", {"fp.pl_beacon", plural_parameter}}
local tooltip = {"", {"fp.tt_title", beacon_proto.localised_name}, number_line, metadata.beacon_tutorial_tt}
local style = (line.done) and "flib_slot_button_grayscale_small" or "flib_slot_button_default_small"
beacon_flow.add{type="sprite-button", sprite=beacon_proto.sprite, number=line.beacon.amount,
tooltip=tooltip, tags={mod="fp", on_gui_click="act_on_compact_beacon", type="beacon", line_id=line.id},
style=style, mouse_button_filter={"left-and-right"}}
add_modules_flow(beacon_flow, "beacon", line, metadata)
end
end
local function add_item_flow(line, relevant_line, item_class, button_color, metadata, item_buttons)
local column_count = metadata.column_counts[item_class]
if column_count == 0 then metadata.parent.add{type="empty-widget"}; return end
local item_table = metadata.parent.add{type="table", column_count=column_count}
for _, item in ipairs(Line.get_in_order(line, item_class)) do
local proto, type = item.proto, item.proto.type
-- items/s/machine does not make sense for lines with subfloors, show items/s instead
local machine_count = (not line.subfloor) and line.machine.count or nil
local amount, number_tooltip = view_state.process_item(metadata.view_state_metadata, item, nil, machine_count)
if amount == -1 then goto skip_item end -- an amount of -1 means it was below the margin of error
local number_line = (number_tooltip) and {"", "\n", number_tooltip} or ""
local tooltip = {"", {"fp.tt_title", proto.localised_name}, number_line, metadata.item_tutorial_tt}
local style, enabled = "flib_slot_button_" .. button_color .. "_small", true
if relevant_line.done then style = "flib_slot_button_grayscale_small" end
if type == "entity" then
style = (relevant_line.done) and "flib_slot_button_transparent_grayscale_small"
or "flib_slot_button_transparent_small"
enabled = false
tooltip = {"", {"fp.tt_title_with_note", proto.localised_name, {"fp.raw_ore"}}, number_line}
end
local button = item_table.add{type="sprite-button", sprite=proto.sprite, number=amount, tooltip=tooltip,
tags={mod="fp", on_gui_click="act_on_compact_item", on_gui_hover="hover_compact_item",
on_gui_leave="leave_compact_item", line_id=line.id, class=item.class, item_id=item.id},
style=style, enabled=enabled, mouse_button_filter={"left-and-right"}}
button.raise_hover_events = true
item_buttons[type] = item_buttons[type] or {}
item_buttons[type][proto.name] = item_buttons[type][proto.name] or {}
table.insert(item_buttons[type][proto.name], {button=button, proper_style=style})
::skip_item::
end
if item_class == "Ingredient" and not line.subfloor and line.machine.fuel then
local fuel, machine_count = line.machine.fuel, line.machine.count
local amount, number_tooltip = view_state.process_item(metadata.view_state_metadata, fuel, nil, machine_count)
if amount == -1 then goto skip_fuel end -- an amount of -1 means it was below the margin of error
local name_line = {"fp.tt_title_with_note", fuel.proto.localised_name, {"fp.pl_fuel", 1}}
local number_line = (number_tooltip) and {"", "\n", number_tooltip} or ""
local tooltip = {"", name_line, number_line, metadata.item_tutorial_tt}
local style = (relevant_line.done) and "flib_slot_button_grayscale_small" or "flib_slot_button_cyan_small"
item_table.add{type="sprite-button", sprite=fuel.proto.sprite, style=style, number=amount,
tags={mod="fp", on_gui_click="act_on_compact_item", line_id=line.id, class="Fuel"},
tooltip=tooltip, mouse_button_filter={"left-and-right"}}
::skip_fuel::
end
end
local function refresh_compact_subfactory(player)
local player_table = util.globals.player_table(player)
local compact_elements = player_table.ui_state.compact_elements
local context = player_table.ui_state.context
local subfactory = context.subfactory
if not subfactory or not subfactory.valid then return end
local current_level = subfactory.selected_floor.level
local lines = Floor.get_in_order(context.floor, "Line")
util.raise.refresh(player, "view_state", compact_elements.view_state_table)
local attach_subfactory_products = player_table.preferences.attach_subfactory_products
compact_elements.name_label.caption = Subfactory.tostring(subfactory, attach_subfactory_products, true)
compact_elements.level_label.caption = {"fp.bold_label", {"", "- ", {"fp.level"}, " ", current_level}}
compact_elements.floor_up_button.enabled = (current_level > 1)
compact_elements.floor_top_button.enabled = (current_level > 1)
local production_table = compact_elements.production_table
production_table.clear()
-- Available columns for items only, as recipe and machines can't be 'compressed'
local frame_width = compact_elements.compact_frame.style.maximal_width
local available_columns = determine_available_columns(lines, frame_width)
if available_columns < 2 then available_columns = 2 end -- fix for too many modules or too high of a GUI scale
local column_counts = determine_column_counts(lines, available_columns)
local metadata = {
parent = production_table,
column_counts = column_counts,
view_state_metadata = view_state.generate_metadata(player, subfactory)
}
compact_elements.item_buttons = {} -- (re)set the item_buttons table
local item_buttons = compact_elements.item_buttons
if util.globals.preferences(player).tutorial_mode then
util.actions.tutorial_tooltip_list(metadata, player, {
recipe_tutorial_tt = "act_on_compact_recipe",
module_tutorial_tt = "act_on_compact_module",
machine_tutorial_tt = "act_on_compact_machine",
beacon_tutorial_tt = "act_on_compact_beacon",
item_tutorial_tt = "act_on_compact_item",
})
end
for _, line in ipairs(lines) do -- build the individual lines
local relevant_line = (line.subfloor) and line.subfloor.defining_line or line
if not relevant_line.active then goto skip_line end
-- Recipe and Checkmark
local recipe_flow = production_table.add{type="flow", direction="horizontal"}
recipe_flow.style.vertical_align = "center"
add_checkmark_button(recipe_flow, line, relevant_line)
add_recipe_button(recipe_flow, line, relevant_line, metadata)
-- Machine and Beacon
local machines_flow = production_table.add{type="flow", direction="vertical"}
add_machine_flow(machines_flow, line, metadata)
add_beacon_flow(machines_flow, line, metadata)
-- Products, Byproducts and Ingredients
add_item_flow(line, relevant_line, "Product", "default", metadata, item_buttons)
add_item_flow(line, relevant_line, "Byproduct", "red", metadata, item_buttons)
add_item_flow(line, relevant_line, "Ingredient", "green", metadata, item_buttons)
production_table.add{type="empty-widget", style="flib_horizontal_pusher"}
::skip_line::
end
end
local function build_compact_subfactory(player)
local ui_state = util.globals.ui_state(player)
local compact_elements = ui_state.compact_elements
-- Content frame
local content_frame = compact_elements.compact_frame.add{type="frame", direction="vertical",
style="inside_deep_frame"}
content_frame.style.vertically_stretchable = true
local subheader = content_frame.add{type="frame", direction="vertical", style="subheader_frame"}
subheader.style.maximal_height = 100 -- large value to nullify maximal_height
-- Flow view state
local flow_view_state = subheader.add{type="flow", direction="horizontal"}
flow_view_state.style.padding = {4, 4, 0, 0}
flow_view_state.add{type="empty-widget", style="flib_horizontal_pusher"}
view_state.rebuild_state(player) -- initializes the view_state
util.raise.build(player, "view_state", flow_view_state)
compact_elements["view_state_table"] = flow_view_state["table_view_state"]
subheader.add{type="line", direction="horizontal"}
-- Flow navigation
local flow_navigation = subheader.add{type="flow", direction="horizontal"}
flow_navigation.style.vertical_align = "center"
flow_navigation.style.margin = {4, 8}
local label_name = flow_navigation.add{type="label"}
label_name.style.font = "heading-2"
label_name.style.maximal_width = 260
compact_elements["name_label"] = label_name
local label_level = flow_navigation.add{type="label"}
label_level.style.margin = {0, 6, 0, 6}
compact_elements["level_label"] = label_level
local button_floor_up = flow_navigation.add{type="sprite-button", sprite="fp_sprite_arrow_line_up",
tooltip={"fp.floor_up_tt"}, tags={mod="fp", on_gui_click="change_compact_floor", destination="up"},
style="fp_sprite-button_rounded_mini", mouse_button_filter={"left"}}
compact_elements["floor_up_button"] = button_floor_up
local button_floor_top = flow_navigation.add{type="sprite-button", sprite="fp_sprite_arrow_line_bar_up",
tooltip={"fp.floor_top_tt"}, tags={mod="fp", on_gui_click="change_compact_floor", destination="top"},
style="fp_sprite-button_rounded_mini", mouse_button_filter={"left"}}
compact_elements["floor_top_button"] = button_floor_top
-- Production table
local scroll_pane_production = content_frame.add{type="scroll-pane", direction="vertical",
style="flib_naked_scroll_pane_no_padding"}
scroll_pane_production.style.horizontally_stretchable = true
local table_production = scroll_pane_production.add{type="table", column_count=6, style="fp_table_production"}
table_production.vertical_centering = false
table_production.style.horizontal_spacing = 12
table_production.style.vertical_spacing = 8
table_production.style.padding = {4, 8}
compact_elements["production_table"] = table_production
refresh_compact_subfactory(player)
end
local function handle_recipe_click(player, tags, action)
local context = util.globals.context(player)
local line = Floor.get(context.floor, "Line", tags.line_id)
local relevant_line = (line.subfloor) and line.subfloor.defining_line or line
local recipe = relevant_line.recipe
if action == "open_subfloor" then
if line.subfloor then
util.context.set_floor(player, line.subfloor)
refresh_compact_subfactory(player)
end
elseif action == "recipebook" then
util.open_in_recipebook(player, "recipe", recipe.proto.name)
end
end
local function handle_module_click(player, tags, action)
local context = util.globals.context(player)
local line = Floor.get(context.floor, "Line", tags.line_id)
-- I don't need to care about relevant lines here because this only gets called on lines without subfloor
local parent_entity = line[tags.parent_type]
local module = ModuleSet.get(parent_entity.module_set, tags.module_id)
if action == "recipebook" then
util.open_in_recipebook(player, "item", module.proto.name)
end
end
local function handle_machine_click(player, tags, action)
local context = util.globals.context(player)
local line = Floor.get(context.floor, "Line", tags.line_id)
-- I don't need to care about relevant lines here because this only gets called on lines without subfloor
if action == "put_into_cursor" then
util.cursor.set_entity(player, line, line.machine)
elseif action == "recipebook" then
util.open_in_recipebook(player, "entity", line.machine.proto.name)
end
end
local function handle_beacon_click(player, tags, action)
local context = util.globals.context(player)
local line = Floor.get(context.floor, "Line", tags.line_id)
-- I don't need to care about relevant lines here because this only gets called on lines without subfloor
if action == "put_into_cursor" then
util.cursor.set_entity(player, line, line.beacon)
elseif action == "recipebook" then
util.open_in_recipebook(player, "entity", line.beacon.proto.name)
end
end
local function handle_item_click(player, tags, action)
local context = util.globals.context(player)
local line = Floor.get(context.floor, "Line", tags.line_id)
local item = (tags.class == "Fuel") and line.machine.fuel or Line.get(line, tags.class, tags.item_id)
if action == "put_into_cursor" then
util.cursor.add_to_item_combinator(player, item.proto, item.amount)
elseif action == "recipebook" then
util.open_in_recipebook(player, item.proto.type, item.proto.name)
end
end
local function handle_hover_change(player, tags, event)
local ui_state = util.globals.ui_state(player)
local line = Floor.get(ui_state.context.floor, "Line", tags.line_id)
local proto = Line.get(line, tags.class, tags.item_id).proto
local relevant_buttons = ui_state.compact_elements.item_buttons[proto.type][proto.name]
for _, button_data in pairs(relevant_buttons) do
button_data.button.style = (event.name == defines.events.on_gui_hover)
and "flib_slot_button_pink_small" or button_data.proper_style
end
end
-- ** EVENTS **
local subfactory_listeners = {}
subfactory_listeners.gui = {
on_gui_click = {
{
name = "change_compact_floor",
handler = (function(player, tags, _)
local floor_changed = util.context.change_floor(player, tags.destination)
if floor_changed then refresh_compact_subfactory(player) end
end)
},
{
name = "act_on_compact_recipe",
modifier_actions = {
open_subfloor = {"left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_recipe_click
},
{
name = "act_on_compact_module",
modifier_actions = {
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_module_click
},
{
name = "act_on_compact_machine",
modifier_actions = {
put_into_cursor = {"left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_machine_click
},
{
name = "act_on_compact_beacon",
modifier_actions = {
put_into_cursor = {"left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_beacon_click
},
{
name = "act_on_compact_item",
modifier_actions = {
put_into_cursor = {"left"},
recipebook = {"alt-right", {recipebook=true}}
},
handler = handle_item_click
}
},
on_gui_checked_state_changed = {
{
name = "checkmark_compact_line",
handler = (function(player, tags, _)
local line = Floor.get(util.globals.context(player).floor, "Line", tags.line_id)
local relevant_line = (line.subfloor) and line.subfloor.defining_line or line
relevant_line.done = not relevant_line.done
refresh_compact_subfactory(player)
end)
}
},
on_gui_hover = {
{
name = "hover_compact_item",
handler = handle_hover_change
}
},
on_gui_leave = {
{
name = "leave_compact_item",
handler = handle_hover_change
}
}
}
subfactory_listeners.misc = {
build_gui_element = (function(player, event)
if event.trigger == "compact_subfactory" then
build_compact_subfactory(player)
end
end),
refresh_gui_element = (function(player, event)
if event.trigger == "compact_subfactory" then
refresh_compact_subfactory(player)
end
end)
}
-- The frame surrounding the main part of the compact subfactory
local frame_dimensions = {width = 0.25, height = 0.8} -- as a percentage of the screen
local frame_location = {x = 10, y = 63} -- absolute, relative to 1080p with scale 1
-- ** LOCAL UTIL **
-- Set frame dimensions in a relative way, taking player resolution and scaling into account
local function set_compact_frame_dimensions(player, frame)
local resolution, scale = player.display_resolution, player.display_scale
local actual_resolution = {width=math.ceil(resolution.width / scale), height=math.ceil(resolution.height / scale)}
frame.style.width = actual_resolution.width * frame_dimensions.width
frame.style.maximal_height = actual_resolution.height * frame_dimensions.height
end
local function set_compact_frame_location(player, frame)
local scale = player.display_scale
frame.location = {frame_location.x * scale, frame_location.y * scale}
end
local function rebuild_compact_dialog(player, default_visibility)
local ui_state = util.globals.ui_state(player)
local compact_elements = ui_state.compact_elements
local interface_visible = default_visibility
local compact_frame = compact_elements.compact_frame
-- Delete the existing interface if there is one
if compact_frame ~= nil then
if compact_frame.valid then
interface_visible = compact_frame.visible
compact_frame.destroy()
end
ui_state.compact_elements = {} -- reset all compact element references
compact_elements = ui_state.compact_elements
end
local frame_compact_dialog = player.gui.screen.add{type="frame", direction="vertical",
visible=interface_visible, name="fp_frame_compact_dialog"}
set_compact_frame_location(player, frame_compact_dialog)
set_compact_frame_dimensions(player, frame_compact_dialog)
compact_elements["compact_frame"] = frame_compact_dialog
-- Title bar
local flow_title_bar = frame_compact_dialog.add{type="flow", direction="horizontal",
tags={mod="fp", on_gui_click="place_compact_dialog"}}
flow_title_bar.style.horizontal_spacing = 8
flow_title_bar.drag_target = frame_compact_dialog
flow_title_bar.add{type="label", caption={"mod-name.factoryplanner"}, style="frame_title",
ignored_by_interaction=true}
flow_title_bar.add{type="empty-widget", style="flib_titlebar_drag_handle",
ignored_by_interaction=true}
local button_switch = flow_title_bar.add{type="sprite-button", style="frame_action_button",
tags={mod="fp", on_gui_click="switch_to_main_view"}, tooltip={"fp.switch_to_main_view"},
sprite="fp_sprite_arrow_right_light", hovered_sprite="fp_sprite_arrow_right_dark",
clicked_sprite="fp_sprite_arrow_right_dark", mouse_button_filter={"left"}}
button_switch.style.padding = 2
local button_close = flow_title_bar.add{type="sprite-button", tags={mod="fp", on_gui_click="close_compact_dialog"},
sprite="utility/close_white", hovered_sprite="utility/close_black", clicked_sprite="utility/close_black",
tooltip={"fp.close_interface"}, style="frame_action_button", mouse_button_filter={"left"}}
button_close.style.padding = 1
util.raise.build(player, "compact_subfactory", nil)
return frame_compact_dialog
end
-- ** TOP LEVEL **
compact_dialog = {}
function compact_dialog.toggle(player)
local ui_state = util.globals.ui_state(player)
local frame_compact_dialog = ui_state.compact_elements.compact_frame
-- Doesn't set player.opened so other GUIs like the inventory can be opened when building
if frame_compact_dialog == nil or not frame_compact_dialog.valid then
rebuild_compact_dialog(player, true) -- refreshes on its own
else
local new_dialog_visibility = not frame_compact_dialog.visible
frame_compact_dialog.visible = new_dialog_visibility
if new_dialog_visibility then refresh_compact_subfactory(player) end
end
end
function compact_dialog.is_in_focus(player)
local frame_compact_dialog = util.globals.ui_state(player).compact_elements.compact_frame
return (frame_compact_dialog ~= nil and frame_compact_dialog.valid and frame_compact_dialog.visible)
end
-- ** EVENTS **
local dialog_listeners = {}
dialog_listeners.gui = {
on_gui_click = {
{
name = "switch_to_main_view",
handler = (function(player, _, _)
util.globals.flags(player).compact_view = false
compact_dialog.toggle(player)
main_dialog.toggle(player)
util.raise.refresh(player, "production", nil)
end)
},
{
name = "close_compact_dialog",
handler = (function(player, _, _)
compact_dialog.toggle(player)
end)
},
{
name = "place_compact_dialog",
handler = (function(player, _, event)
if event.button == defines.mouse_button_type.middle then
local ui_state = util.globals.ui_state(player)
local frame_compact_dialog = ui_state.compact_elements.compact_frame
set_compact_frame_location(player, frame_compact_dialog)
end
end)
}
}
}
dialog_listeners.misc = {
on_player_display_resolution_changed = (function(player, _)
rebuild_compact_dialog(player, false)
end),
on_player_display_scale_changed = (function(player, _)
rebuild_compact_dialog(player, false)
end),
on_lua_shortcut = (function(player, event)
if event.prototype_name == "fp_open_interface" and util.globals.flags(player).compact_view then
compact_dialog.toggle(player)
end
end),
fp_toggle_interface = (function(player, _)
if util.globals.flags(player).compact_view then compact_dialog.toggle(player) end
end)
}
return { subfactory_listeners, dialog_listeners }

View File

@@ -0,0 +1,258 @@
require("ui.main.subfactory_list")
require("ui.base.view_state")
main_dialog = {}
-- Accepts custom width and height parameters so dimensions can be tried out without needing to change actual settings
local function determine_main_dimensions(player, products_per_row, subfactory_list_rows)
local settings = util.globals.settings(player)
products_per_row = products_per_row or settings.products_per_row
subfactory_list_rows = subfactory_list_rows or settings.subfactory_list_rows
local frame_spacing = MAGIC_NUMBERS.frame_spacing
-- Width of the larger ingredients-box, which has twice the buttons per row
local boxes_width_1 = (products_per_row * 2 * MAGIC_NUMBERS.item_button_size) + (2 * frame_spacing)
-- Width of the two smaller product+byproduct-boxes
local boxes_width_2 = 2 * ((products_per_row * MAGIC_NUMBERS.item_button_size) + (2 * frame_spacing))
local width = MAGIC_NUMBERS.list_width + boxes_width_1 + boxes_width_2 + ((2+3) * frame_spacing)
local subfactory_list_height = (subfactory_list_rows * MAGIC_NUMBERS.list_element_height)
+ MAGIC_NUMBERS.subheader_height
local height = MAGIC_NUMBERS.title_bar_height + subfactory_list_height + MAGIC_NUMBERS.info_height
+ ((2+1) * frame_spacing)
return {width=width, height=height}
end
-- Downscale width and height mod settings until the main interface fits onto the player's screen
function main_dialog.shrinkwrap_interface(player)
local resolution, scale = player.display_resolution, player.display_scale
local actual_resolution = {width=math.ceil(resolution.width / scale), height=math.ceil(resolution.height / scale)}
local mod_settings = util.globals.settings(player)
local products_per_row = mod_settings.products_per_row
local subfactory_list_rows = mod_settings.subfactory_list_rows
local function dimensions() return determine_main_dimensions(player, products_per_row, subfactory_list_rows) end
while (actual_resolution.width * 0.95) < dimensions().width do
products_per_row = products_per_row - 1
end
while (actual_resolution.height * 0.95) < dimensions().height do
subfactory_list_rows = subfactory_list_rows - 2
end
local setting_prototypes = game.mod_setting_prototypes
local width_minimum = setting_prototypes["fp_products_per_row"].allowed_values[1] --[[@as number]]
local height_minimum = setting_prototypes["fp_subfactory_list_rows"].allowed_values[1] --[[@as number]]
local live_settings = settings.get_player_settings(player)
live_settings["fp_products_per_row"] = {value = math.max(products_per_row, width_minimum)}
live_settings["fp_subfactory_list_rows"] = {value = math.max(subfactory_list_rows, height_minimum)}
end
local function interface_toggle(metadata)
local player = game.get_player(metadata.player_index)
local compact_view = util.globals.flags(player).compact_view
if compact_view then compact_dialog.toggle(player)
else main_dialog.toggle(player) end
end
function main_dialog.rebuild(player, default_visibility)
local ui_state = util.globals.ui_state(player)
local main_elements = ui_state.main_elements
local interface_visible = default_visibility
local main_frame = main_elements.main_frame
-- Delete the existing interface if there is one
if main_frame ~= nil then
if main_frame.valid then
interface_visible = main_frame.visible
main_frame.destroy()
end
ui_state.main_elements = {} -- reset all main element references
main_elements = ui_state.main_elements
end
-- Create and configure the top-level frame
local frame_main_dialog = player.gui.screen.add{type="frame", direction="vertical",
visible=interface_visible, tags={mod="fp", on_gui_closed="close_main_dialog"},
name="fp_frame_main_dialog"}
main_elements["main_frame"] = frame_main_dialog
local dimensions = determine_main_dimensions(player)
ui_state.main_dialog_dimensions = dimensions
frame_main_dialog.style.size = dimensions
util.gui.properly_center_frame(player, frame_main_dialog, dimensions)
-- Create the actual dialog structure
local frame_spacing = MAGIC_NUMBERS.frame_spacing
main_elements.flows = {}
local top_horizontal = frame_main_dialog.add{type="flow", direction="horizontal"}
main_elements.flows["top_horizontal"] = top_horizontal
local main_horizontal = frame_main_dialog.add{type="flow", direction="horizontal"}
main_horizontal.style.horizontal_spacing = frame_spacing
main_elements.flows["main_horizontal"] = main_horizontal
local left_vertical = main_horizontal.add{type="flow", direction="vertical"}
left_vertical.style.vertical_spacing = frame_spacing
main_elements.flows["left_vertical"] = left_vertical
local right_vertical = main_horizontal.add{type="flow", direction="vertical"}
right_vertical.style.vertical_spacing = frame_spacing
main_elements.flows["right_vertical"] = right_vertical
view_state.rebuild_state(player) -- initializes the view_state
util.raise.build(player, "main_dialog", nil) -- tells all elements to build themselves
if interface_visible then player.opened = frame_main_dialog end
main_dialog.set_pause_state(player, frame_main_dialog)
end
function main_dialog.toggle(player, skip_opened)
local ui_state = util.globals.ui_state(player)
local frame_main_dialog = ui_state.main_elements.main_frame
if frame_main_dialog == nil or not frame_main_dialog.valid then
main_dialog.rebuild(player, true) -- sets opened and paused-state itself
elseif ui_state.modal_dialog_type == nil then -- don't toggle if modal dialog is open
local new_dialog_visibility = not frame_main_dialog.visible
frame_main_dialog.visible = new_dialog_visibility
if not skip_opened then -- flag used only for hacky internal reasons
player.opened = (new_dialog_visibility) and frame_main_dialog or nil
end
main_dialog.set_pause_state(player, frame_main_dialog)
-- Make sure FP is not behind some vanilla interfaces
if new_dialog_visibility then frame_main_dialog.bring_to_front() end
end
end
-- Returns true when the main dialog is open while no modal dialogs are
function main_dialog.is_in_focus(player)
local frame_main_dialog = util.globals.main_elements(player).main_frame
return (frame_main_dialog ~= nil and frame_main_dialog.valid and frame_main_dialog.visible
and util.globals.ui_state(player).modal_dialog_type == nil)
end
-- Sets the game.paused-state as is appropriate
function main_dialog.set_pause_state(player, frame_main_dialog, force_false)
-- Don't touch paused-state if this is a multiplayer session or the editor is active
if game.is_multiplayer() or player.controller_type == defines.controllers.editor then return end
game.tick_paused = (util.globals.preferences(player).pause_on_interface and not force_false)
and frame_main_dialog.visible or false
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_closed = {
{
name = "close_main_dialog",
handler = (function(player, _, _)
main_dialog.toggle(player)
end)
}
},
on_gui_click = {
{
name = "mod_gui_toggle_interface",
handler = (function(player, _, _)
if DEV_ACTIVE then -- implicit mod reload for easier development
util.gui.reset_player(player) -- destroys all FP GUIs
util.gui.toggle_mod_gui(player) -- fixes the mod gui button after its been destroyed
game.reload_mods() -- toggle needs to be delayed by a tick since the reload is not instant
game.print("Mods reloaded")
util.nth_tick.register((game.tick + 1), "interface_toggle", {player_index=player.index})
else -- call the interface toggle function directly
interface_toggle({player_index=player.index})
end
end)
}
}
}
listeners.misc = {
-- Makes sure that another GUI can open properly while a modal dialog is open.
-- The FP interface can have at most 3 layers of GUI: main interface, modal dialog, selection mode.
-- We need to make sure opening the technology screen (for example) from any of those layers behaves properly.
-- We need to consider that if the technology screen is opened (which is the reason we get this event),
-- the game automtically closes the currently open GUI before calling this one. This means the top layer
-- that's open at that stage is closed already when we get here. So we're at most at the modal dialog
-- layer at this point and need to close the things below, if there are any.
on_gui_opened = (function(player, _)
local ui_state = util.globals.ui_state(player)
-- With that in mind, if there's a modal dialog open, we were in selection mode, and need to close the dialog
if ui_state.modal_dialog_type ~= nil then util.raise.close_dialog(player, "cancel", true) end
-- Then, at this point we're at most at the stage where the main dialog is open, so close it
if main_dialog.is_in_focus(player) then main_dialog.toggle(player, true) end
end),
on_player_display_resolution_changed = (function(player, _)
main_dialog.shrinkwrap_interface(player)
main_dialog.rebuild(player, false)
end),
on_player_display_scale_changed = (function(player, _)
main_dialog.shrinkwrap_interface(player)
main_dialog.rebuild(player, false)
end),
on_lua_shortcut = (function(player, event)
if event.prototype_name == "fp_open_interface" and not util.globals.flags(player).compact_view then
main_dialog.toggle(player)
end
end),
fp_toggle_interface = (function(player, _)
if not util.globals.flags(player).compact_view then main_dialog.toggle(player) end
end),
-- This needs to be in a single place, otherwise the events cancel each other out
fp_toggle_compact_view = (function(player, _)
local ui_state = util.globals.ui_state(player)
local flags = ui_state.flags
local subfactory = ui_state.context.subfactory
local main_focus = main_dialog.is_in_focus(player)
local compact_focus = compact_dialog.is_in_focus(player)
-- Open the compact view if this toggle is pressed when neither dialog
-- is open as that makes the most sense from a user perspective
if not main_focus and not compact_focus then
flags.compact_view = true
compact_dialog.toggle(player)
elseif flags.compact_view and compact_focus then
compact_dialog.toggle(player)
main_dialog.toggle(player)
util.raise.refresh(player, "production", nil)
flags.compact_view = false
elseif main_focus and subfactory ~= nil and subfactory.valid then
main_dialog.toggle(player)
compact_dialog.toggle(player) -- toggle also refreshes
flags.compact_view = true
end
end)
}
listeners.global = {
interface_toggle = interface_toggle
}
return { listeners }

View File

@@ -0,0 +1,372 @@
modal_dialog = {}
---@alias ModalDialogType string
---@class ModalData: table
-- ** LOCAL UTIL **
local function create_base_modal_dialog(player, dialog_settings, modal_data)
local modal_elements = modal_data.modal_elements
local frame_modal_dialog = player.gui.screen.add{type="frame", direction="vertical",
tags={mod="fp", on_gui_closed="close_modal_dialog"}}
frame_modal_dialog.style.minimal_width = 240
modal_elements.modal_frame = frame_modal_dialog
-- Title bar
if dialog_settings.caption ~= nil then
local flow_title_bar = frame_modal_dialog.add{type="flow", direction="horizontal",
tags={mod="fp", on_gui_click="re-center_modal_dialog"}}
flow_title_bar.drag_target = frame_modal_dialog
flow_title_bar.add{type="label", caption=dialog_settings.caption, style="frame_title",
ignored_by_interaction=true}
flow_title_bar.add{type="empty-widget", style="flib_titlebar_drag_handle", ignored_by_interaction=true}
if dialog_settings.search_handler_name then -- add a search field if requested
modal_data.search_handler_name = dialog_settings.search_handler_name
modal_data.next_search_tick = nil -- used for rate limited search
local searchfield = flow_title_bar.add{type="textfield", style="search_popup_textfield",
tags={mod="fp", on_gui_text_changed="modal_searchfield"}}
searchfield.style.width = 140
searchfield.style.top_margin = -3
util.gui.setup_textfield(searchfield)
modal_elements.search_textfield = searchfield
modal_dialog.set_searchfield_state(player)
local search_button = flow_title_bar.add{type="sprite-button", tooltip={"fp.search_button_tt"},
tags={mod="fp", on_gui_click="focus_modal_searchfield"}, sprite="utility/search_white",
hovered_sprite="utility/search_black", clicked_sprite="utility/search_black",
style="frame_action_button", mouse_button_filter={"left"}}
search_button.style.left_margin = 4
end
if not dialog_settings.show_submit_button then -- add X-to-close button if this is not a submit dialog
local close_button = flow_title_bar.add{type="sprite-button", tooltip={"fp.close_button_tt"},
tags={mod="fp", on_gui_click="close_modal_dialog", action="cancel"}, sprite="utility/close_white",
hovered_sprite="utility/close_black", clicked_sprite="utility/close_black", style="frame_action_button",
mouse_button_filter={"left"}}
close_button.style.left_margin = 4
close_button.style.padding = 1
end
end
-- Content frame
local main_content_element = nil
if dialog_settings.create_content_frame then
local content_frame = frame_modal_dialog.add{type="frame", direction="vertical", style="inside_shallow_frame"}
content_frame.style.vertically_stretchable = true
if dialog_settings.subheader_text then
local subheader = content_frame.add{type="frame", direction="horizontal", style="subheader_frame"}
subheader.style.horizontally_stretchable = true
subheader.style.padding = {12, 24, 12, 12}
local label = subheader.add{type="label", caption=dialog_settings.subheader_text,
tooltip=dialog_settings.subheader_tooltip}
label.style.font = "default-semibold"
end
local scroll_pane = content_frame.add{type="scroll-pane", direction="vertical", style="flib_naked_scroll_pane"}
if dialog_settings.disable_scroll_pane then scroll_pane.vertical_scroll_policy = "never" end
modal_elements.content_frame = scroll_pane
main_content_element = scroll_pane
else -- if no content frame is created, simply add a flow that the dialog can add to instead
local flow = frame_modal_dialog.add{type="flow", direction="vertical"}
modal_elements.dialog_flow = flow
main_content_element = flow
end
-- Set the maximum height of the main content element
local dialog_max_height = (util.globals.ui_state(player).main_dialog_dimensions.height - 80) * 0.95
modal_data.dialog_maximal_height = dialog_max_height
main_content_element.style.maximal_height = dialog_max_height
if dialog_settings.show_submit_button then -- if there is a submit button, there should be a button bar
-- Button bar
local button_bar = frame_modal_dialog.add{type="flow", direction="horizontal",
style="dialog_buttons_horizontal_flow"}
button_bar.style.horizontal_spacing = 0
-- Cancel button
local button_cancel = button_bar.add{type="button", tags={mod="fp", on_gui_click="close_modal_dialog",
action="cancel"}, style="back_button", caption={"fp.cancel"}, tooltip={"fp.cancel_dialog_tt"},
mouse_button_filter={"left"}}
button_cancel.style.minimal_width = 0
button_cancel.style.padding = {1, 12, 0, 12}
-- Delete button and spacers
if dialog_settings.show_delete_button then
local left_drag_handle = button_bar.add{type="empty-widget", style="flib_dialog_footer_drag_handle"}
left_drag_handle.drag_target = frame_modal_dialog
local button_delete = button_bar.add{type="button", caption={"fp.delete"}, style="red_button",
tags={mod="fp", on_gui_click="close_modal_dialog", action="delete"}, mouse_button_filter={"left"}}
button_delete.style.font = "default-dialog-button"
button_delete.style.height = 32
button_delete.style.minimal_width = 0
button_delete.style.padding = {0, 8}
-- If there is a delete button present, we need to set a minimum dialog width for it to look good
frame_modal_dialog.style.minimal_width = 340
end
-- One 'drag handle' should always be visible
local right_drag_handle = button_bar.add{type="empty-widget", style="flib_dialog_footer_drag_handle"}
right_drag_handle.drag_target = frame_modal_dialog
-- Submit button
local button_submit = button_bar.add{type="button", tags={mod="fp", on_gui_click="close_modal_dialog",
action="submit"}, caption={"fp.submit"}, tooltip={"fp.confirm_dialog_tt"}, style="confirm_button",
mouse_button_filter={"left"}}
button_submit.style.minimal_width = 0
button_submit.style.padding = {1, 8, 0, 12}
modal_elements.dialog_submit_button = button_submit
end
return frame_modal_dialog
end
local function run_delayed_modal_search(metadata)
local player = game.get_player(metadata.player_index)
local modal_data = util.globals.modal_data(player)
if not modal_data or not modal_data.modal_elements then return end
local searchfield = modal_data.modal_elements.search_textfield
local search_term = searchfield.text:gsub("^%s*(.-)%s*$", "%1"):lower()
GLOBAL_HANDLERS[modal_data.search_handler_name](player, search_term)
end
-- ** TOP LEVEL **
-- Opens a barebone modal dialog and calls upon the given function to populate it
function modal_dialog.enter(player, metadata, dialog_open, early_abort_check)
local ui_state = util.globals.ui_state(player)
if ui_state.modal_dialog_type ~= nil then
-- If a dialog is currently open, and this one wants to be queued, do so
if metadata.allow_queueing then ui_state.queued_dialog_metadata = metadata end
return
end
ui_state.modal_data = metadata.modal_data or {}
if early_abort_check ~= nil and early_abort_check(player, ui_state.modal_data) then -- abort early if need be
--ui_state.modal_data = nil -- this should be reset, but that breaks the stupid queueing stuff .........
return
end
ui_state.modal_dialog_type = metadata.dialog
ui_state.modal_data.modal_elements = {}
ui_state.modal_data.confirmed_dialog = false
-- Create interface_dimmer first so the layering works out correctly
local interface_dimmer = player.gui.screen.add{type="frame", style="fp_frame_semitransparent",
tags={mod="fp", on_gui_click="re-layer_interface_dimmer"}, visible=(not metadata.skip_dimmer)}
interface_dimmer.style.size = ui_state.main_dialog_dimensions
interface_dimmer.location = ui_state.main_elements.main_frame.location
ui_state.modal_data.modal_elements.interface_dimmer = interface_dimmer
-- Create modal dialog framework and let the dialog itself fill it out
local frame_modal_dialog = create_base_modal_dialog(player, metadata, ui_state.modal_data)
dialog_open(player, ui_state.modal_data)
player.opened = frame_modal_dialog
frame_modal_dialog.force_auto_center() -- seems to be necessary now, not sure why
end
-- Handles the closing process of a modal dialog, reopening the main dialog thereafter
function modal_dialog.exit(player, action, skip_opened, dialog_close)
local ui_state = util.globals.ui_state(player) -- dialog guaranteed to be open
local modal_elements = ui_state.modal_data.modal_elements
local submit_button = modal_elements.dialog_submit_button
-- Stop exiting if trying to submit while submission is disabled
if action == "submit" and (submit_button and not submit_button.enabled) then return end
-- Call the closing function for this dialog, if it has one
if dialog_close ~= nil then dialog_close(player, action) end
-- Unregister the delayed search handler if present
local search_tick = ui_state.modal_data.next_search_tick
if search_tick ~= nil then util.nth_tick.cancel(search_tick) end
ui_state.modal_dialog_type = nil
ui_state.modal_data = nil
modal_elements.interface_dimmer.destroy()
modal_elements.modal_frame.destroy()
ui_state.modal_elements = nil
if not skip_opened then player.opened = ui_state.main_elements.main_frame end
if ui_state.queued_dialog_metadata ~= nil then
util.raise.open_dialog(player, ui_state.queued_dialog_metadata)
ui_state.queued_dialog_metadata = nil
end
end
function modal_dialog.set_searchfield_state(player)
local player_table = util.globals.player_table(player)
if not player_table.ui_state.modal_dialog_type then return end
local searchfield = player_table.ui_state.modal_data.modal_elements.search_textfield
if not searchfield then return end
local status = (player_table.translation_tables ~= nil)
searchfield.enabled = status -- disables on nil and false
searchfield.tooltip = (status) and {"fp.searchfield_tt"} or {"fp.warning_with_icon", {"fp.searchfield_not_ready_tt"}}
end
function modal_dialog.set_submit_button_state(modal_elements, enabled, message)
local caption = (enabled) and {"fp.submit"} or {"fp.warning_with_icon", {"fp.submit"}}
local tooltip = (enabled) and {"fp.confirm_dialog_tt"} or {"fp.warning_with_icon", message}
local button = modal_elements.dialog_submit_button
button.style.left_padding = (enabled) and 12 or 6
button.enabled = enabled
button.caption = caption
button.tooltip = tooltip
end
function modal_dialog.enter_selection_mode(player, selector_name)
local ui_state = util.globals.ui_state(player)
ui_state.flags.selection_mode = true
player.cursor_stack.set_stack(selector_name)
local frame_main_dialog = ui_state.main_elements.main_frame
frame_main_dialog.visible = false
main_dialog.set_pause_state(player, frame_main_dialog, true)
local modal_elements = ui_state.modal_data.modal_elements
modal_elements.interface_dimmer.visible = false
modal_elements.modal_frame.ignored_by_interaction = true
modal_elements.modal_frame.location = {25, 50}
end
function modal_dialog.leave_selection_mode(player)
local ui_state = util.globals.ui_state(player)
ui_state.flags.selection_mode = false
player.cursor_stack.set_stack(nil)
local modal_elements = ui_state.modal_data.modal_elements
modal_elements.interface_dimmer.visible = true
-- player.opened needs to be set because on_gui_closed sets it to nil
player.opened = modal_elements.modal_frame
modal_elements.modal_frame.ignored_by_interaction = false
modal_elements.modal_frame.force_auto_center()
local frame_main_dialog = ui_state.main_elements.main_frame
frame_main_dialog.visible = true
main_dialog.set_pause_state(player, frame_main_dialog)
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "re-layer_interface_dimmer",
handler = (function(player, _, _)
util.globals.modal_elements(player).modal_frame.bring_to_front()
end)
},
{
name = "re-center_modal_dialog",
handler = (function(player, _, event)
if event.button == defines.mouse_button_type.middle then
local modal_elements = util.globals.modal_elements(player)
modal_elements.modal_frame.force_auto_center()
end
end)
},
{
name = "close_modal_dialog",
handler = (function(player, tags, _)
util.raise.close_dialog(player, tags.action)
end)
},
{
name = "focus_modal_searchfield",
handler = (function(player, _, _)
util.gui.select_all(util.globals.modal_elements(player).search_textfield)
end)
}
},
on_gui_text_changed = {
{
name = "modal_searchfield",
timeout = MAGIC_NUMBERS.modal_search_rate_limit,
handler = (function(player, _, metadata)
local modal_data = util.globals.modal_data(player)
local search_tick = modal_data.search_tick
if search_tick ~= nil then util.nth_tick.cancel(search_tick) end
local search_term = metadata.text:gsub("^%s*(.-)%s*$", "%1"):lower()
GLOBAL_HANDLERS[modal_data.search_handler_name](player, search_term)
-- Set up delayed search update to circumvent issues caused by rate limiting
local desired_tick = game.tick + MAGIC_NUMBERS.modal_search_rate_limit
modal_data.next_search_tick = util.nth_tick.register(desired_tick,
"run_delayed_modal_search", {player_index=player.index})
end)
}
},
on_gui_closed = {
{
name = "close_modal_dialog",
handler = (function(player, _, event)
local ui_state = util.globals.ui_state(player)
if ui_state.flags.selection_mode then
modal_dialog.leave_selection_mode(player)
else
-- Here, we need to distinguish between submitting a dialog with E or ESC
util.raise.close_dialog(player, (ui_state.modal_data.confirmed_dialog) and "submit" or "cancel")
-- If the dialog was not closed, it means submission was disabled, and we need to re-set .opened
if event.element.valid then player.opened = event.element end
end
-- Reset .confirmed_dialog if this event didn't actually lead to the dialog closing
if event.element.valid then ui_state.modal_data.confirmed_dialog = false end
end)
}
}
}
listeners.misc = {
fp_confirm_dialog = (function(player, _)
if not util.globals.flags(player).selection_mode then
util.raise.close_dialog(player, "submit")
end
end),
fp_confirm_gui = (function(player, _)
-- Note that a GUI was closed by confirming, so it'll try submitting on_gui_closed
local modal_data = util.globals.modal_data(player)
if modal_data ~= nil then modal_data.confirmed_dialog = true end
end),
fp_focus_searchfield = (function(player, _)
local ui_state = util.globals.ui_state(player)
if ui_state.modal_dialog_type ~= nil then
local textfield_search = ui_state.modal_data.modal_elements.search_textfield
if textfield_search then util.gui.select_all(textfield_search) end
end
end)
}
listeners.global = {
run_delayed_modal_search = run_delayed_modal_search
}
return { listeners }

View File

@@ -0,0 +1,276 @@
-- This contains both the UI handling for view states, as well as the amount conversions
local timescale_map = {[1] = "second", [60] = "minute", [3600] = "hour"}
---@class ViewStates: table
-- ** LOCAL UTIL **
local function cycle_views(player, direction)
local ui_state = util.globals.ui_state(player)
if ui_state.view_states and main_dialog.is_in_focus(player) or compact_dialog.is_in_focus(player) then
local selected_view_id, view_state_count = ui_state.view_states.selected_view_id, #ui_state.view_states
local new_view_id = nil -- need to make sure this is wrapped properly in either direction
if direction == "standard" then
new_view_id = (selected_view_id == view_state_count) and 1 or (selected_view_id + 1)
else -- direction == "reverse"
new_view_id = (selected_view_id == 1) and view_state_count or (selected_view_id - 1)
end
view_state.select(player, new_view_id)
local compact_view = util.globals.flags(player).compact_view
local refresh = (compact_view) and "compact_subfactory" or "production"
util.raise.refresh(player, refresh, nil)
-- This avoids the game focusing a random textfield when pressing Tab to change states
local main_frame = ui_state.main_elements.main_frame
if main_frame ~= nil then main_frame.focus() end
end
end
local processors = {} -- individual functions for each kind of view state
function processors.items_per_timescale(metadata, raw_amount, item_proto, _)
local number = util.format.number(raw_amount, metadata.formatting_precision)
local plural_parameter = (number == "1") and 1 or 2
local type_string = (item_proto.type == "fluid") and {"fp.l_fluid"} or {"fp.pl_item", plural_parameter}
local tooltip = {"", number, " ", type_string, "/", metadata.timescale_string}
return number, tooltip
end
function processors.belts_or_lanes(metadata, raw_amount, item_proto, _)
if item_proto.type == "entity" then return nil, nil end -- raw ores don't make sense here
local divisor = (item_proto.type == "fluid") and 50 or 1
local raw_number = raw_amount * metadata.throughput_multiplier * metadata.timescale_inverse / divisor
local number = util.format.number(raw_number, metadata.formatting_precision)
local plural_parameter = (number == "1") and 1 or 2
local tooltip = {"", number, " ", {"fp.pl_" .. metadata.belt_or_lane, plural_parameter}}
local return_number = (metadata.round_button_numbers) and math.ceil(raw_number - 0.001) or number
return return_number, tooltip
end
function processors.wagons_per_timescale(metadata, raw_amount, item_proto, _)
if item_proto.type == "entity" then return nil, nil end -- raw ores don't make sense here
local wagon_capacity = (item_proto.type == "fluid") and metadata.fluid_wagon_capacity
or metadata.cargo_wagon_capactiy * item_proto.stack_size
local wagon_count = raw_amount / wagon_capacity
local number = util.format.number(wagon_count, metadata.formatting_precision)
local plural_parameter = (number == "1") and 1 or 2
local tooltip = {"", number, " ", {"fp.pl_wagon", plural_parameter}, "/", metadata.timescale_string}
return number, tooltip
end
function processors.items_per_second_per_machine(metadata, raw_amount, item_proto, machine_count)
if machine_count == 0 then return 0, "" end -- avoid division by zero
if item_proto.type == "entity" then return nil, nil end -- raw ores don't make sense here
local raw_number = raw_amount * metadata.timescale_inverse / (math.ceil((machine_count or 1) - 0.001))
local number = util.format.number(raw_number, metadata.formatting_precision)
local plural_parameter = (number == "1") and 1 or 2
local type_string = (item_proto.type == "fluid") and {"fp.l_fluid"} or {"fp.pl_item", plural_parameter}
-- If machine_count is nil, this shouldn't show /machine
local per_machine = (machine_count ~= nil) and {"", "/", {"fp.pl_machine", 1}} or ""
local tooltip = {"", number, " ", type_string, "/", {"fp.second"}, per_machine}
return number, tooltip
end
local function refresh_view_state(player, table_view_state)
local ui_state = util.globals.ui_state(player)
-- Automatically detects a timescale change and refreshes the state if necessary
local subfactory = ui_state.context.subfactory
if not subfactory then
return
elseif subfactory.current_timescale ~= ui_state.view_states.timescale then
view_state.rebuild_state(player)
end
for _, view_button in ipairs(table_view_state.children) do
local view_state = ui_state.view_states[view_button.tags.view_id]
view_button.caption = view_state.caption
view_button.tooltip = view_state.tooltip
view_button.toggled = (view_state.selected)
end
end
local function build_view_state(player, parent_element)
local view_states = util.globals.ui_state(player).view_states
local table_view_state = parent_element.add{type="table", name="table_view_state", column_count=#view_states}
table_view_state.style.horizontal_spacing = 0
-- Using ipairs is important as we only want to iterate the array-part
for view_id, _ in ipairs(view_states) do
local button = table_view_state.add{type="button", style="fp_button_push", mouse_button_filter={"left"},
tags={mod="fp", on_gui_click="change_view_state", view_id=view_id}}
button.style.padding = {0, 12}
end
end
-- ** TOP LEVEL **
view_state = {}
-- Creates metadata relevant for a whole batch of items
function view_state.generate_metadata(player, subfactory)
local player_table = util.globals.player_table(player)
local view_states = player_table.ui_state.view_states
local current_view_name = view_states[view_states.selected_view_id].name
local belts_or_lanes = player_table.settings.belts_or_lanes
local round_button_numbers = player_table.preferences.round_button_numbers
local throughput = prototyper.defaults.get(player, "belts").throughput
local throughput_divisor = (belts_or_lanes == "belts") and throughput or (throughput / 2)
local default_cargo_wagon = prototyper.defaults.get(player, "wagons", PROTOTYPE_MAPS.wagons["cargo-wagon"].id)
local default_fluid_wagon = prototyper.defaults.get(player, "wagons", PROTOTYPE_MAPS.wagons["fluid-wagon"].id)
return {
processor = processors[current_view_name],
timescale_inverse = 1 / subfactory.timescale,
timescale_string = {"fp." .. timescale_map[subfactory.timescale]},
adjusted_margin_of_error = MAGIC_NUMBERS.margin_of_error * subfactory.timescale,
belt_or_lane = belts_or_lanes:sub(1, -2),
round_button_numbers = round_button_numbers,
throughput_multiplier = 1 / throughput_divisor,
formatting_precision = 4,
cargo_wagon_capactiy = default_cargo_wagon.storage,
fluid_wagon_capacity = default_fluid_wagon.storage
}
end
function view_state.process_item(metadata, item, item_amount, machine_count)
local raw_amount = item_amount or item.amount
if raw_amount == nil or (raw_amount ~= 0 and raw_amount < metadata.adjusted_margin_of_error) then
return -1, nil
end
return metadata.processor(metadata, raw_amount, item.proto, machine_count)
end
function view_state.rebuild_state(player)
local ui_state = util.globals.ui_state(player)
local subfactory = ui_state.context.subfactory
-- If no subfactory exists yet, choose a default timescale so the UI can build properly
local timescale = (subfactory) and timescale_map[subfactory.timescale] or "second"
local singular_bol = util.globals.settings(player).belts_or_lanes:sub(1, -2)
local belt_proto = prototyper.defaults.get(player, "belts")
local default_cargo_wagon = prototyper.defaults.get(player, "wagons", PROTOTYPE_MAPS.wagons["cargo-wagon"].id)
local default_fluid_wagon = prototyper.defaults.get(player, "wagons", PROTOTYPE_MAPS.wagons["fluid-wagon"].id)
local new_view_states = {
[1] = {
name = "items_per_timescale",
caption = {"", {"fp.pu_item", 2}, "/", {"fp.unit_" .. timescale}},
tooltip = {"fp.view_state_tt", {"fp.items_per_timescale", {"fp." .. timescale}}}
},
[2] = {
name = "belts_or_lanes",
caption = {"", belt_proto.rich_text, " ", {"fp.pu_" .. singular_bol, 2}},
tooltip = {"fp.view_state_tt", {"fp.belts_or_lanes", {"fp.pl_" .. singular_bol, 2},
belt_proto.rich_text, belt_proto.localised_name}}
},
[3] = {
name = "wagons_per_timescale",
caption = {"", {"fp.pu_wagon", 2}, "/", {"fp.unit_" .. timescale}},
tooltip = {"fp.view_state_tt", {"fp.wagons_per_timescale", {"fp." .. timescale},
default_cargo_wagon.rich_text, default_cargo_wagon.localised_name,
default_fluid_wagon.rich_text, default_fluid_wagon.localised_name}}
},
[4] = {
name = "items_per_second_per_machine",
caption = {"", {"fp.pu_item", 2}, "/", {"fp.unit_second"}, "/[img=fp_generic_assembler]"},
tooltip = {"fp.view_state_tt", {"fp.items_per_second_per_machine"}}
},
selected_view_id = nil, -- set below
timescale = timescale -- conserve the timescale to rebuild the state
}
-- Conserve the previous view selection if possible
local old_view_states = ui_state.view_states
local selected_view_id = (old_view_states) and old_view_states.selected_view_id or "items_per_timescale"
ui_state.view_states = new_view_states
view_state.select(player, selected_view_id)
end
function view_state.select(player, selected_view)
local view_states = util.globals.ui_state(player).view_states
-- Selected view can be either an id or a name, so we might need to match an id to a name
local selected_view_id = selected_view
if type(selected_view) == "string" then
for view_id, view_state in ipairs(view_states) do
if view_state.name == selected_view then
selected_view_id = view_id
break
end
end
end
-- Only run any code if the selected view did indeed change
if view_states.selected_view_id ~= selected_view_id then
for view_id, view_state in ipairs(view_states) do
if view_id == selected_view_id then
view_states.selected_view_id = selected_view_id
view_state.selected = true
else
view_state.selected = false
end
end
end
end
-- ** EVENTS **
local listeners = {}
listeners.gui = {
on_gui_click = {
{
name = "change_view_state",
handler = (function(player, tags, _)
view_state.select(player, tags.view_id)
local compact_view = util.globals.flags(player).compact_view
local refresh = (compact_view) and "compact_subfactory" or "production"
util.raise.refresh(player, refresh, nil)
end)
}
}
}
listeners.misc = {
fp_cycle_production_views = (function(player, _)
cycle_views(player, "standard")
end),
fp_reverse_cycle_production_views = (function(player, _)
cycle_views(player, "reverse")
end),
build_gui_element = (function(player, event)
if event.trigger == "view_state" then
build_view_state(player, event.parent)
end
end),
refresh_gui_element = (function(player, event)
if event.trigger == "view_state" then
refresh_view_state(player, event.element)
end
end)
}
return { listeners }