325 lines
9.6 KiB
GDScript
325 lines
9.6 KiB
GDScript
class_name DataManagementDialog
|
|
extends Control
|
|
|
|
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
|
|
|
signal action_selected(action_id: StringName)
|
|
signal dismissed
|
|
|
|
enum ActionTone {
|
|
SECONDARY,
|
|
PRIMARY,
|
|
DANGER,
|
|
}
|
|
|
|
const DEFAULT_SIZE := Vector2(600.0, 330.0)
|
|
const MINIMUM_SIZE := Vector2(520.0, 250.0)
|
|
const MAXIMUM_SIZE := Vector2(680.0, 500.0)
|
|
const ACTION_HEIGHT: float = 44.0
|
|
|
|
var _dimmer: ColorRect
|
|
var _panel: PanelContainer
|
|
var _title_label: Label
|
|
var _body_scroll: ScrollContainer
|
|
var _body: VBoxContainer
|
|
var _description: Label
|
|
var _custom_content: VBoxContainer
|
|
var _actions: VBoxContainer
|
|
var _action_buttons: Array[Button] = []
|
|
var _cancel_action_id: StringName = StringName("")
|
|
var _action_callback: Callable
|
|
var _presentation_generation: int = 0
|
|
|
|
|
|
func _ready() -> void:
|
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
|
# Settings panels are hosted inside several differently layered title and
|
|
# pause containers. Keep the data modal above that whole presentation, not
|
|
# merely above the panel that opened it.
|
|
z_index = 250
|
|
_build_layout()
|
|
hide()
|
|
|
|
|
|
func present(
|
|
title_text: String,
|
|
body_text: String,
|
|
actions: Array[Dictionary],
|
|
action_callback: Callable = Callable(),
|
|
custom_content: Control = null,
|
|
preferred_control: Control = null,
|
|
requested_size: Vector2 = DEFAULT_SIZE,
|
|
) -> void:
|
|
if not is_inside_tree() or is_queued_for_deletion():
|
|
push_warning("Data-management dialog is not available yet.")
|
|
return
|
|
if not is_node_ready():
|
|
call_deferred(
|
|
"present",
|
|
title_text,
|
|
body_text,
|
|
actions,
|
|
action_callback,
|
|
custom_content,
|
|
preferred_control,
|
|
requested_size,
|
|
)
|
|
return
|
|
_presentation_generation += 1
|
|
_action_callback = action_callback
|
|
_title_label.text = title_text
|
|
_description.text = body_text
|
|
_description.visible = not body_text.strip_edges().is_empty()
|
|
_replace_custom_content(custom_content)
|
|
_rebuild_actions(actions)
|
|
_panel.custom_minimum_size = _clamped_size(requested_size)
|
|
show()
|
|
_focus_preferred.call_deferred(preferred_control)
|
|
|
|
|
|
func dismiss() -> void:
|
|
if not visible:
|
|
return
|
|
_presentation_generation += 1
|
|
_action_callback = Callable()
|
|
_cancel_action_id = StringName("")
|
|
hide()
|
|
dismissed.emit()
|
|
|
|
|
|
func is_open() -> bool:
|
|
return visible
|
|
|
|
|
|
func cancel() -> bool:
|
|
if not visible:
|
|
return false
|
|
if _cancel_action_id.is_empty():
|
|
dismiss()
|
|
return true
|
|
_trigger_action(_cancel_action_id)
|
|
return true
|
|
|
|
|
|
func focused_control() -> Control:
|
|
if not is_inside_tree():
|
|
return null
|
|
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
|
return focus_owner if focus_owner != null and is_ancestor_of(focus_owner) else null
|
|
|
|
|
|
func _build_layout() -> void:
|
|
_dimmer = ColorRect.new()
|
|
_dimmer.name = "Dimmer"
|
|
_dimmer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
_dimmer.color = Color(0.0, 0.04, 0.06, 0.72)
|
|
_dimmer.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
add_child(_dimmer)
|
|
|
|
var center := CenterContainer.new()
|
|
center.name = "Center"
|
|
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
center.mouse_filter = Control.MOUSE_FILTER_PASS
|
|
add_child(center)
|
|
|
|
_panel = PanelContainer.new()
|
|
_panel.name = "Panel"
|
|
_panel.custom_minimum_size = DEFAULT_SIZE
|
|
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
_panel.add_theme_stylebox_override(
|
|
"panel", UtilityPageStyleType.panel_style()
|
|
)
|
|
center.add_child(_panel)
|
|
|
|
var layout := VBoxContainer.new()
|
|
layout.name = "Layout"
|
|
layout.add_theme_constant_override("separation", 14)
|
|
_panel.add_child(layout)
|
|
|
|
_title_label = Label.new()
|
|
_title_label.name = "Title"
|
|
_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
_title_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
_title_label.add_theme_font_override("font", UtilityPageStyleType.TuffyFont)
|
|
_title_label.add_theme_font_size_override("font_size", 29)
|
|
_title_label.add_theme_color_override(
|
|
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
|
|
)
|
|
layout.add_child(_title_label)
|
|
|
|
_body_scroll = ScrollContainer.new()
|
|
_body_scroll.name = "BodyScroll"
|
|
_body_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
_body_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
_body_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
|
_body_scroll.follow_focus = true
|
|
layout.add_child(_body_scroll)
|
|
|
|
_body = VBoxContainer.new()
|
|
_body.name = "Body"
|
|
_body.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
_body.add_theme_constant_override("separation", 12)
|
|
_body_scroll.add_child(_body)
|
|
|
|
_description = Label.new()
|
|
_description.name = "Description"
|
|
_description.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
_description.add_theme_font_override("font", UtilityPageStyleType.TuffyFont)
|
|
_description.add_theme_font_size_override("font_size", 18)
|
|
_description.add_theme_color_override(
|
|
"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
|
|
)
|
|
_body.add_child(_description)
|
|
|
|
_custom_content = VBoxContainer.new()
|
|
_custom_content.name = "CustomContent"
|
|
_custom_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
_custom_content.add_theme_constant_override("separation", 12)
|
|
_body.add_child(_custom_content)
|
|
|
|
_actions = VBoxContainer.new()
|
|
_actions.name = "Actions"
|
|
_actions.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
layout.add_child(_actions)
|
|
|
|
|
|
func _replace_custom_content(content: Control) -> void:
|
|
for child: Node in _custom_content.get_children():
|
|
_custom_content.remove_child(child)
|
|
child.queue_free()
|
|
if content == null:
|
|
return
|
|
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
_custom_content.add_child(content)
|
|
|
|
|
|
func _rebuild_actions(actions: Array[Dictionary]) -> void:
|
|
for child: Node in _actions.get_children():
|
|
_actions.remove_child(child)
|
|
child.queue_free()
|
|
_action_buttons.clear()
|
|
_cancel_action_id = StringName("")
|
|
var action_ids: Array[StringName] = []
|
|
var action_layout: BoxContainer
|
|
if actions.size() <= 2:
|
|
var row := HBoxContainer.new()
|
|
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
row.add_theme_constant_override("separation", 12)
|
|
action_layout = row
|
|
else:
|
|
var column := VBoxContainer.new()
|
|
column.add_theme_constant_override("separation", 10)
|
|
action_layout = column
|
|
_actions.add_child(action_layout)
|
|
for entry: Dictionary in actions:
|
|
var action_id: StringName = StringName(str(entry.get("id", "")))
|
|
if action_id.is_empty():
|
|
continue
|
|
var button := Button.new()
|
|
button.name = "%sButton" % str(action_id).capitalize()
|
|
button.text = str(entry.get("text", action_id))
|
|
button.disabled = bool(entry.get("disabled", false))
|
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
button.custom_minimum_size = Vector2(0.0, ACTION_HEIGHT)
|
|
button.focus_mode = Control.FOCUS_ALL
|
|
_apply_action_style(button, int(entry.get("tone", ActionTone.PRIMARY)))
|
|
button.set_meta("data_action_id", action_id)
|
|
button.pressed.connect(_trigger_action.bind(action_id))
|
|
action_layout.add_child(button)
|
|
_action_buttons.append(button)
|
|
action_ids.append(action_id)
|
|
if bool(entry.get("cancel", false)):
|
|
_cancel_action_id = action_id
|
|
if _cancel_action_id.is_empty() and not action_ids.is_empty():
|
|
_cancel_action_id = action_ids.back()
|
|
|
|
|
|
func _apply_action_style(button: Button, tone: int) -> void:
|
|
UtilityPageStyleType.apply_ocean_button(button)
|
|
match tone:
|
|
ActionTone.SECONDARY:
|
|
for state: StringName in [&"normal", &"hover", &"pressed", &"focus"]:
|
|
var color := (
|
|
UtilityPageStyleType.OCEAN_PANEL_DEEP
|
|
if state == &"normal"
|
|
else UtilityPageStyleType.OCEAN_PANEL_MID
|
|
)
|
|
button.add_theme_stylebox_override(
|
|
state,
|
|
UtilityPageStyleType.ocean_button_style(color),
|
|
)
|
|
ActionTone.DANGER:
|
|
button.add_theme_stylebox_override(
|
|
"normal",
|
|
UtilityPageStyleType.ocean_button_style(
|
|
UtilityPageStyleType.OCEAN_DANGER
|
|
),
|
|
)
|
|
button.add_theme_stylebox_override(
|
|
"hover",
|
|
UtilityPageStyleType.ocean_button_style(Color("c95a64")),
|
|
)
|
|
button.add_theme_stylebox_override(
|
|
"pressed",
|
|
UtilityPageStyleType.ocean_button_style(Color("74313b")),
|
|
)
|
|
button.add_theme_stylebox_override(
|
|
"focus",
|
|
UtilityPageStyleType.ocean_button_style(Color("c95a64")),
|
|
)
|
|
|
|
|
|
func _trigger_action(action_id: StringName) -> void:
|
|
if not visible:
|
|
return
|
|
var generation: int = _presentation_generation
|
|
action_selected.emit(action_id)
|
|
if _action_callback.is_valid():
|
|
_action_callback.call(action_id)
|
|
if generation == _presentation_generation and visible:
|
|
dismiss()
|
|
|
|
|
|
func _focus_preferred(preferred_control: Control) -> void:
|
|
if not visible:
|
|
return
|
|
var focusable: Array[Control] = []
|
|
for node: Node in _custom_content.find_children("*", "Control", true, false):
|
|
var control := node as Control
|
|
if _is_focusable(control):
|
|
focusable.append(control)
|
|
for button: Button in _action_buttons:
|
|
if _is_focusable(button):
|
|
focusable.append(button)
|
|
for index: int in focusable.size():
|
|
var control: Control = focusable[index]
|
|
control.focus_previous = control.get_path_to(
|
|
focusable[wrapi(index - 1, 0, focusable.size())]
|
|
)
|
|
control.focus_next = control.get_path_to(
|
|
focusable[wrapi(index + 1, 0, focusable.size())]
|
|
)
|
|
if _is_focusable(preferred_control):
|
|
preferred_control.grab_focus()
|
|
elif not focusable.is_empty():
|
|
focusable.front().grab_focus()
|
|
|
|
|
|
func _is_focusable(control: Control) -> bool:
|
|
if (
|
|
control == null
|
|
or not control.is_visible_in_tree()
|
|
or control.focus_mode != Control.FOCUS_ALL
|
|
):
|
|
return false
|
|
var button := control as BaseButton
|
|
return button == null or not button.disabled
|
|
|
|
|
|
func _clamped_size(requested_size: Vector2) -> Vector2:
|
|
return Vector2(
|
|
clampf(requested_size.x, MINIMUM_SIZE.x, MAXIMUM_SIZE.x),
|
|
clampf(requested_size.y, MINIMUM_SIZE.y, MAXIMUM_SIZE.y),
|
|
)
|