373 lines
16 KiB
Lua
373 lines
16 KiB
Lua
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 }
|