570 lines
17 KiB
Lua

local flib_gui = require("__flib__/gui-lite")
local flib_position = require("__flib__/position")
local flib_table = require("__flib__/table")
local gui_rates = require("__RateCalculator__/scripts/gui-rates")
local gui_util = require("__RateCalculator__/scripts/gui-util")
--- @class GuiData
--- @field elems table<string, LuaGuiElement>
--- @field inserter_divisor string
--- @field manual_multiplier double
--- @field materials_divisor string?
--- @field pinned boolean
--- @field player LuaPlayer
--- @field search_open boolean
--- @field search_query string
--- @field selected_set_index integer
--- @field selected_timescale Timescale
--- @field sets CalculationSet[]
--- @field transport_belt_divisor string
--- @field display_data_lookup DisplayDataLookup
--- @type GuiLocation
local top_left_location = { x = 15, y = 58 + 15 }
--- @param self GuiData
local function reset_location(self)
local value = self.player.mod_settings["rcalc-default-gui-location"].value
local window = self.elems.rcalc_window
if value == "top-left" then
local scale = self.player.display_scale
window.location = flib_position.mul(top_left_location, { scale, scale })
else
window.auto_center = true
end
end
--- @param self GuiData
local function update_gui(self)
local sets = self.sets
local selected_set_index = self.selected_set_index
local set = sets[selected_set_index]
if not set then
return
end
local elems = self.elems
local nav_backward_button = elems.nav_backward_button
local at_back = selected_set_index == 1
nav_backward_button.sprite = at_back and "flib_nav_backward_disabled" or "flib_nav_backward_white"
nav_backward_button.enabled = not at_back
nav_backward_button.tooltip = { "gui.rcalc-previous-set", selected_set_index, #sets }
local nav_forward_button = elems.nav_forward_button
local at_front = selected_set_index == #sets
nav_forward_button.sprite = at_front and "flib_nav_forward_disabled" or "flib_nav_forward_white"
nav_forward_button.enabled = not at_front
nav_forward_button.tooltip = { "gui.rcalc-next-set", selected_set_index, #sets }
local timescale = self.selected_timescale
local timescale_data = gui_util.timescale_data[timescale]
local timescale_divisor_chooser = elems.timescale_divisor_chooser
local divisor_source = timescale_data.divisor_source
if divisor_source then
timescale_divisor_chooser.visible = true
timescale_divisor_chooser.elem_filters = global.elem_filters[divisor_source]
timescale_divisor_chooser.elem_value = self[divisor_source]
else
timescale_divisor_chooser.visible = false
end
elems.timescale_dropdown.selected_index = flib_table.find(gui_util.ordered_timescales, timescale) --[[@as uint]]
elems.multiplier_textfield.text = tostring(self.manual_multiplier)
set.errors["inserter-rates-estimates"] = divisor_source == "inserter_divisor" and true or nil
local show_checkboxes = self.player.mod_settings["rcalc-show-completion-checkboxes"].value --[[@as boolean]]
local show_intermediate_breakdowns = self.player.mod_settings["rcalc-show-intermediate-breakdowns"].value --[[@as boolean]]
self.elems.rates_scroll_pane.style.minimal_width = 500
+ (show_checkboxes and 44 or 0)
+ (show_intermediate_breakdowns and 50 or 0)
local category_display_data = gui_rates.update_display_data(self, set)
gui_rates.update_gui(self, category_display_data)
local errors_frame = self.elems.errors_frame
errors_frame.clear()
local visible = false
if self.player.mod_settings["rcalc-show-calculation-errors"].value then
for error in pairs(set.errors) do
visible = true
errors_frame.add({
type = "label",
style = "bold_label",
caption = { "", "[img=warning-white] ", { "gui.rcalc-error-" .. error } },
tooltip = { "?", { "gui.rcalc-error-" .. error .. "-description" }, "" },
})
end
end
errors_frame.visible = visible
end
--- @param self GuiData
local function toggle_search(self)
local search_open = not self.search_open
self.search_open = search_open
local button = self.elems.search_button
button.sprite = search_open and "utility/search_black" or "utility/search_white"
button.style = search_open and "flib_selected_frame_action_button" or "frame_action_button"
local textfield = self.elems.search_textfield
textfield.visible = search_open
self.search_open = search_open
if search_open then
textfield.focus()
textfield.select_all()
else
textfield.text = ""
self.search_query = ""
update_gui(self)
end
end
--- @param e EventData.on_gui_click
local function on_window_closed(e)
local self = global.gui[e.player_index]
if not self or self.pinned then
return
end
if self.search_open then
toggle_search(self)
self.player.opened = self.elems.rcalc_window
return
end
self.elems.timescale_dropdown.close_dropdown()
self.elems.rcalc_window.visible = false
end
--- @param e EventData.on_gui_click
local function on_titlebar_click(e)
local self = global.gui[e.player_index]
if not self or e.button ~= defines.mouse_button_type.middle then
return
end
reset_location(self)
end
--- @param e EventData.on_gui_click
local function on_close_button_click(e)
local self = global.gui[e.player_index]
if not self then
return
end
self.elems.rcalc_window.visible = false
if self.player.opened == self.elems.rcalc_window then
self.player.opened = nil
end
end
--- @param e EventData.on_gui_click
local function on_pin_button_click(e)
local self = global.gui[e.player_index]
if not self then
return
end
local pinned = not self.pinned
e.element.sprite = pinned and "flib_pin_black" or "flib_pin_white"
e.element.style = pinned and "flib_selected_frame_action_button" or "frame_action_button"
self.pinned = pinned
if pinned then
self.player.opened = nil
self.elems.close_button.tooltip = { "gui.close" }
self.elems.search_button.tooltip = { "gui.search" }
else
self.player.opened = self.elems.rcalc_window
self.elems.close_button.tooltip = { "gui.close-instruction" }
self.elems.search_button.tooltip = { "gui.flib-search-instruction" }
end
end
--- @param e EventData.on_gui_click
local function on_nav_backward_button_click(e)
local self = global.gui[e.player_index]
if not self then
return
end
self.selected_set_index = math.max(self.selected_set_index - 1, 1)
update_gui(self)
end
--- @param e EventData.on_gui_click
local function on_nav_forward_button_click(e)
local self = global.gui[e.player_index]
if not self then
return
end
self.selected_set_index = math.min(self.selected_set_index + 1, #self.sets)
update_gui(self)
end
--- @param e EventData.on_gui_click
local function on_search_button_click(e)
local self = global.gui[e.player_index]
if not self then
return
end
toggle_search(self)
end
--- @param e EventData.on_gui_text_changed
local function on_search_text_changed(e)
local self = global.gui[e.player_index]
if not self then
return
end
self.search_query = string.lower(e.text)
update_gui(self)
end
--- @param e EventData.on_gui_elem_changed
local function on_divisor_elem_changed(e)
local self = global.gui[e.player_index]
if not self then
return
end
local entity_name = e.element.elem_value --[[@as string?]]
local timescale = self.selected_timescale
local timescale_data = gui_util.timescale_data[timescale]
if timescale_data.divisor_required and not entity_name then
e.element.elem_value = self[timescale_data.divisor_source]
return
end
self[timescale_data.divisor_source] = entity_name
update_gui(self)
end
--- @param e EventData.on_gui_selection_state_changed
local function on_timescale_dropdown_changed(e)
local self = global.gui[e.player_index]
if not self then
return
end
local new_timescale = gui_util.ordered_timescales[e.element.selected_index]
self.selected_timescale = new_timescale
update_gui(self)
end
--- @param e EventData.on_gui_text_changed
local function on_multiplier_textfield_changed(e)
local self = global.gui[e.player_index]
if not self then
return
end
local text = e.text
-- Don't prevent insertion of a decimal point or zeroes
local last_char = string.sub(text, #text)
if last_char == "." or (string.match(text, "%.") and last_char == "0") then
return
end
local new_value = tonumber(text)
if not new_value or new_value == 0 then
return
end
self.manual_multiplier = new_value
update_gui(self)
end
--- @param e EventData.on_gui_click
local function on_multiplier_nudge_clicked(e)
local self = global.gui[e.player_index]
if not self then
return
end
self.manual_multiplier = math.max(1, math.floor(self.manual_multiplier) + e.element.tags.delta)
update_gui(self)
end
--- @param name string
--- @param sprite SpritePath
--- @param tooltip LocalisedString
--- @param handler GuiElemHandler
--- @return GuiElemDef
local function frame_action_button(name, sprite, tooltip, handler)
return {
type = "sprite-button",
name = name,
style = "frame_action_button",
sprite = sprite .. "_white",
hovered_sprite = sprite .. "_black",
clicked_sprite = sprite .. "_black",
tooltip = tooltip,
mouse_button_filter = { "left" },
handler = { [defines.events.on_gui_click] = handler },
}
end
--- @param player LuaPlayer
local function destroy_gui(player)
local self = global.gui[player.index]
if not self then
return
end
global.gui[player.index] = nil
local window = self.elems.rcalc_window
if not window.valid then
return
end
window.destroy()
end
--- @param player LuaPlayer
--- @return GuiData
local function build_gui(player)
destroy_gui(player)
local elems = flib_gui.add(player.gui.screen, {
type = "frame",
name = "rcalc_window",
direction = "vertical",
visible = false,
handler = { [defines.events.on_gui_closed] = on_window_closed },
{
type = "flow",
style = "flib_titlebar_flow",
drag_target = "rcalc_window",
handler = { [defines.events.on_gui_click] = on_titlebar_click },
{
type = "label",
style = "frame_title",
caption = { "mod-name.RateCalculator" },
ignored_by_interaction = true,
},
{ type = "empty-widget", style = "flib_titlebar_drag_handle", ignored_by_interaction = true },
{
type = "textfield",
name = "search_textfield",
style = "flib_titlebar_search_textfield",
visible = false,
clear_and_focus_on_right_click = true,
lose_focus_on_confirm = true,
handler = { [defines.events.on_gui_text_changed] = on_search_text_changed },
},
frame_action_button("search_button", "utility/search", { "gui.flib-search-instruction" }, on_search_button_click),
frame_action_button(
"nav_backward_button",
"flib_nav_backward",
{ "gui.rcalc-previous-set" },
on_nav_backward_button_click
),
frame_action_button(
"nav_forward_button",
"flib_nav_forward",
{ "gui.rcalc-next-set" },
on_nav_forward_button_click
),
frame_action_button("pin_button", "flib_pin", { "gui.flib-keep-open" }, on_pin_button_click),
frame_action_button("close_button", "utility/close", { "gui.close-instruction" }, on_close_button_click),
},
{
type = "frame",
style = "inside_shallow_frame",
direction = "vertical",
{
type = "frame",
style = "subheader_frame",
{ type = "label", style = "subheader_caption_label", caption = { "gui.rcalc-timescale" } },
{ type = "empty-widget", style = "flib_horizontal_pusher" },
{
type = "choose-elem-button",
name = "timescale_divisor_chooser",
style = "rcalc_units_choose_elem_button",
elem_type = "entity",
tooltip = { "gui.rcalc-capacity-divisor-description" },
handler = { [defines.events.on_gui_elem_changed] = on_divisor_elem_changed },
},
{
type = "drop-down",
name = "timescale_dropdown",
items = flib_table.map(gui_util.ordered_timescales, function(timescale)
return { "string-mod-setting.rcalc-default-timescale-" .. timescale }
end),
handler = { [defines.events.on_gui_selection_state_changed] = on_timescale_dropdown_changed },
},
{ type = "label", caption = "[img=quantity-multiplier]" },
{
type = "flow",
style = "rcalc_multiplier_holder_flow",
{
type = "textfield",
name = "multiplier_textfield",
style = "rcalc_multiplier_textfield",
numeric = true,
allow_decimal = true,
clear_and_focus_on_right_click = true,
lose_focus_on_confirm = true,
tooltip = { "gui.rcalc-manual-multiplier-description" },
text = "1",
handler = { [defines.events.on_gui_text_changed] = on_multiplier_textfield_changed },
},
{
type = "flow",
style = "rcalc_multiplier_nudge_buttons_flow",
direction = "vertical",
{
type = "sprite-button",
style = "rcalc_multiplier_nudge_button",
sprite = "rcalc_nudge_increase",
tooltip = "+1",
tags = { delta = 1 },
handler = { [defines.events.on_gui_click] = on_multiplier_nudge_clicked },
},
{
type = "sprite-button",
style = "rcalc_multiplier_nudge_button",
sprite = "rcalc_nudge_decrease",
tooltip = "-1",
tags = { delta = -1 },
handler = { [defines.events.on_gui_click] = on_multiplier_nudge_clicked },
},
},
},
},
{
type = "scroll-pane",
name = "rates_scroll_pane",
style = "rcalc_rates_table_scroll_pane",
{ type = "flow", name = "rates_flow", style = "rcalc_rates_table_horizontal_flow" },
},
{
type = "frame",
name = "errors_frame",
style = "rcalc_negative_subfooter_frame",
direction = "vertical",
visible = false,
},
},
})
player.opened = elems.rcalc_window
local default_timescale = player.mod_settings["rcalc-default-timescale"].value --[[@as Timescale]]
--- @type GuiData
local self = {
elems = elems,
inserter_divisor = gui_util.get_first_prototype(global.elem_filters.inserter_divisor),
manual_multiplier = 1,
pinned = false,
player = player,
search_open = false,
search_query = "",
selected_timescale = default_timescale,
sets = {},
transport_belt_divisor = gui_util.get_first_prototype(global.elem_filters.transport_belt_divisor),
}
global.gui[player.index] = self
reset_location(self)
return self
end
--- @param e EventData.on_runtime_mod_setting_changed
local function on_runtime_mod_setting_changed(e)
if not string.match(e.setting, "^rcalc") then
return
end
local self = global.gui[e.player_index]
if not self then
return
end
update_gui(self)
if e.setting == "rcalc-default-gui-location" then
reset_location(self)
end
end
--- @param e EventData.CustomInputEvent
local function on_linked_focus_search(e)
local self = global.gui[e.player_index]
if not self or not self.elems.rcalc_window.valid or self.pinned or not self.elems.rcalc_window.visible then
return
end
toggle_search(self)
end
local gui = {}
--- @param player LuaPlayer
--- @return CalculationSet?
function gui.get_current_set(player)
local self = global.gui[player.index]
if self then
return self.sets[self.selected_set_index]
end
end
--- @param player LuaPlayer
--- @param set CalculationSet?
--- @param new_selection boolean?
function gui.build_and_show(player, set, new_selection)
local self = global.gui[player.index]
if not self or not self.elems.rcalc_window.valid then
self = build_gui(player)
end
local sets = self.sets
if set and (new_selection or not sets[1]) then
sets[#sets + 1] = set
if #sets > 10 then
table.remove(sets, 1)
end
self.selected_set_index = #sets
end
if not sets[self.selected_set_index] then
return
end
if new_selection then
self.manual_multiplier = 1
end
gui.show(self)
end
--- @param self GuiData
function gui.show(self)
update_gui(self)
self.elems.rcalc_window.visible = true
if not self.pinned then
self.player.opened = self.elems.rcalc_window
end
self.elems.rcalc_window.bring_to_front()
end
function gui.on_init()
--- @type table<uint, GuiData>
global.gui = {}
gui_util.build_divisor_filters()
gui_util.build_dictionaries()
end
function gui.on_configuration_changed()
gui_util.build_divisor_filters()
gui_util.build_dictionaries()
for _, player in pairs(game.players) do
destroy_gui(player)
end
end
gui.events = {
[defines.events.on_runtime_mod_setting_changed] = on_runtime_mod_setting_changed,
["rcalc-linked-focus-search"] = on_linked_focus_search,
}
flib_gui.add_handlers({
on_close_button_click = on_close_button_click,
on_divisor_elem_changed = on_divisor_elem_changed,
on_multiplier_nudge_clicked = on_multiplier_nudge_clicked,
on_multiplier_textfield_changed = on_multiplier_textfield_changed,
on_nav_backward_button_click = on_nav_backward_button_click,
on_nav_forward_button_click = on_nav_forward_button_click,
on_pin_button_click = on_pin_button_click,
on_search_button_click = on_search_button_click,
on_search_text_changed = on_search_text_changed,
on_timescale_dropdown_changed = on_timescale_dropdown_changed,
on_titlebar_click = on_titlebar_click,
on_window_closed = on_window_closed,
})
return gui