feat: Add StageDialogStyler for enhanced dialog management
- Introduced StageDialogStyler class to render child-facing dialogs as shared in-stage cards. - Preserved native AcceptDialog/ConfirmationDialog signals for stage workflows. - Implemented methods for applying styles, managing visibility, and handling button actions. - Added support for dynamic theming and responsive design based on viewport size. - Created utility functions for styling buttons and managing dialog content.
This commit is contained in:
@@ -0,0 +1,644 @@
|
||||
class_name StageDialogStyler
|
||||
extends RefCounted
|
||||
## Renders child-facing dialogs as a shared in-stage card while preserving the
|
||||
## native AcceptDialog/ConfirmationDialog signals used by the stage workflows.
|
||||
|
||||
const THEME_DEFAULTS := preload("res://scripts/stage/sandbox_theme.gd")
|
||||
const OVERLAY_META := &"stage_dialog_overlay"
|
||||
const BODY_META := &"stage_dialog_body"
|
||||
const BODY_SCROLL_META := &"stage_dialog_body_scroll"
|
||||
const NOTE_META := &"stage_dialog_note"
|
||||
const DETAIL_META := &"stage_dialog_detail"
|
||||
const ACTIONS_META := &"stage_dialog_actions"
|
||||
const EXTRAS_META := &"stage_dialog_extra_controls"
|
||||
const CARD_META := &"stage_dialog_card"
|
||||
const TITLE_META := &"stage_dialog_title"
|
||||
const ICON_META := &"stage_dialog_icon"
|
||||
const ICON_LABEL_META := &"stage_dialog_icon_label"
|
||||
const ICON_BADGE_META := &"stage_dialog_icon_badge"
|
||||
const PRIMARY_META := &"stage_dialog_primary_button"
|
||||
const DESTRUCTIVE_META := &"stage_dialog_destructive_buttons"
|
||||
const OPEN_META := &"stage_dialog_open"
|
||||
|
||||
|
||||
static func apply(
|
||||
dialog: Window,
|
||||
stage_theme: SandboxTheme,
|
||||
ui_font: Font,
|
||||
primary_button: Button = null,
|
||||
destructive_buttons: Array = []
|
||||
) -> void:
|
||||
if dialog == null:
|
||||
return
|
||||
var active_theme: SandboxTheme = stage_theme if stage_theme != null else THEME_DEFAULTS.new()
|
||||
if not dialog.has_meta(OVERLAY_META):
|
||||
_install_dialog_surface(dialog, active_theme, ui_font)
|
||||
var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null
|
||||
var primary: Button = primary_button if primary_button != null else ok_button
|
||||
dialog.set_meta(PRIMARY_META, primary)
|
||||
dialog.set_meta(DESTRUCTIVE_META, destructive_buttons.duplicate())
|
||||
_refresh_dialog_surface(dialog, active_theme, ui_font, primary, destructive_buttons)
|
||||
|
||||
|
||||
static func popup(dialog: Window) -> void:
|
||||
if dialog == null:
|
||||
return
|
||||
var active_theme: SandboxTheme = _theme_for(dialog)
|
||||
var font: Font = dialog.get_meta(&"stage_dialog_font") as Font if dialog.has_meta(&"stage_dialog_font") else null
|
||||
var primary: Button = dialog.get_meta(PRIMARY_META, null) as Button
|
||||
var destructive_buttons: Array = dialog.get_meta(DESTRUCTIVE_META, []) as Array
|
||||
_refresh_dialog_surface(dialog, active_theme, font, primary, destructive_buttons)
|
||||
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
|
||||
if overlay == null:
|
||||
return
|
||||
dialog.hide()
|
||||
dialog.set_meta(OPEN_META, true)
|
||||
overlay.show()
|
||||
var host_viewport: Viewport = overlay.get_viewport()
|
||||
if host_viewport != null:
|
||||
var viewport_size: Vector2i = Vector2i(host_viewport.get_visible_rect().size)
|
||||
var card: PanelContainer = dialog.get_meta(CARD_META) as PanelContainer
|
||||
if card != null:
|
||||
var card_width: int = active_theme.integer("popups.confirmation.card_width", 560, 320, 720)
|
||||
card.custom_minimum_size.x = minf(float(card_width), maxf(280.0, float(viewport_size.x - 36)))
|
||||
_fit_body_scroll(dialog, viewport_size, active_theme)
|
||||
var tree: SceneTree = dialog.get_tree()
|
||||
if tree != null:
|
||||
var fit_after_final_layout := func() -> void:
|
||||
if not is_instance_valid(dialog) or not dialog.is_inside_tree() or not is_open(dialog):
|
||||
return
|
||||
var viewport_size: Vector2i = Vector2i(host_viewport.get_visible_rect().size) if host_viewport != null else Vector2i.ZERO
|
||||
_fit_body_scroll(dialog, viewport_size, active_theme)
|
||||
var fit_after_layout := func() -> void:
|
||||
if is_instance_valid(tree):
|
||||
tree.process_frame.connect(fit_after_final_layout, Object.CONNECT_ONE_SHOT)
|
||||
var defer_fit_one_frame := func() -> void:
|
||||
if is_instance_valid(tree):
|
||||
tree.process_frame.connect(fit_after_layout, Object.CONNECT_ONE_SHOT)
|
||||
tree.process_frame.connect(defer_fit_one_frame, Object.CONNECT_ONE_SHOT)
|
||||
var focus_primary: Button = dialog.get_meta(PRIMARY_META, null) as Button
|
||||
var focus_target: Button = visual_button(focus_primary)
|
||||
if focus_target != null and focus_target.visible:
|
||||
focus_target.call_deferred("grab_focus")
|
||||
|
||||
|
||||
static func is_open(dialog: Window) -> bool:
|
||||
if dialog == null or not is_instance_valid(dialog):
|
||||
return false
|
||||
if dialog.has_meta(OVERLAY_META):
|
||||
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
|
||||
return overlay != null and is_instance_valid(overlay) and overlay.visible
|
||||
return dialog.visible
|
||||
|
||||
|
||||
static func is_styled(dialog: Window) -> bool:
|
||||
return dialog != null and is_instance_valid(dialog) and dialog.has_meta(OVERLAY_META)
|
||||
|
||||
|
||||
static func hide(dialog: Window) -> void:
|
||||
if dialog == null or not is_instance_valid(dialog):
|
||||
return
|
||||
dialog.set_meta(OPEN_META, false)
|
||||
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control if dialog.has_meta(OVERLAY_META) else null
|
||||
if overlay != null and is_instance_valid(overlay):
|
||||
overlay.hide()
|
||||
if dialog.visible:
|
||||
dialog.hide()
|
||||
|
||||
|
||||
static func dismiss_on_escape(dialog: Window) -> void:
|
||||
if dialog == null or not is_instance_valid(dialog):
|
||||
return
|
||||
if dialog is ConfirmationDialog:
|
||||
var cancel_button: Button = (dialog as ConfirmationDialog).get_cancel_button()
|
||||
var visible_cancel: Button = visual_button(cancel_button)
|
||||
if visible_cancel != null and visible_cancel.visible:
|
||||
visible_cancel.pressed.emit()
|
||||
return
|
||||
if dialog.has_signal("close_requested"):
|
||||
dialog.emit_signal("close_requested")
|
||||
hide(dialog)
|
||||
|
||||
|
||||
static func visual_button(original_button: Button) -> Button:
|
||||
if original_button == null:
|
||||
return null
|
||||
var visual: Variant = original_button.get_meta(&"stage_dialog_visual_button", null)
|
||||
return visual as Button if visual is Button else original_button
|
||||
|
||||
|
||||
static func panel_style(stage_theme: SandboxTheme, include_content_padding: bool = true) -> StyleBoxFlat:
|
||||
var active_theme: SandboxTheme = stage_theme if stage_theme != null else THEME_DEFAULTS.new()
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = active_theme.color("popups.confirmation.surface_color", Color("#202633FA"))
|
||||
style.border_color = active_theme.color("popups.confirmation.border_color", Color("#667386"))
|
||||
style.set_border_width_all(active_theme.integer("popups.confirmation.border_width", 2, 1, 4))
|
||||
style.set_corner_radius_all(active_theme.integer("popups.confirmation.corner_radius", 18, 10, 28))
|
||||
if include_content_padding:
|
||||
var padding: int = active_theme.integer("popups.confirmation.padding", 22, 12, 36)
|
||||
style.content_margin_left = padding
|
||||
style.content_margin_right = padding
|
||||
style.content_margin_top = padding
|
||||
style.content_margin_bottom = padding
|
||||
return style
|
||||
|
||||
|
||||
static func style_button(button: Button, role: StringName, stage_theme: SandboxTheme, ui_font: Font = null) -> void:
|
||||
if button == null:
|
||||
return
|
||||
var active_theme: SandboxTheme = stage_theme if stage_theme != null else THEME_DEFAULTS.new()
|
||||
var action: Color = active_theme.color("popups.confirmation.action_color", Color("#087DBA"))
|
||||
var accent: Color = active_theme.color("popups.confirmation.accent_color", Color("#8BD9FF"))
|
||||
var danger: Color = active_theme.color("popups.confirmation.danger_color", Color("#D94A56"))
|
||||
var secondary: Color = active_theme.color("popups.confirmation.secondary_surface", Color("#293242"))
|
||||
var secondary_border: Color = active_theme.color("popups.confirmation.secondary_border", Color("#718096"))
|
||||
var normal: Color = action
|
||||
var hover: Color = action.lightened(0.12)
|
||||
var pressed: Color = action.darkened(0.12)
|
||||
var border: Color = accent
|
||||
var label_color: Color = Color("#F5F7FA")
|
||||
match role:
|
||||
&"primary":
|
||||
normal = action
|
||||
hover = active_theme.color("stage_panels.action.primary_hover_color", action.lightened(0.12))
|
||||
pressed = active_theme.color("stage_panels.action.primary_pressed_color", action.darkened(0.12))
|
||||
border = accent
|
||||
&"danger":
|
||||
normal = active_theme.color("popups.confirmation.danger_surface", Color("#44262D"))
|
||||
hover = active_theme.color("popups.confirmation.danger_hover_surface", Color("#3B252C"))
|
||||
pressed = active_theme.color("popups.confirmation.danger_pressed_surface", Color("#4A252D"))
|
||||
border = danger
|
||||
label_color = Color("#FFE6E8")
|
||||
_:
|
||||
normal = secondary
|
||||
hover = active_theme.color("stage_panels.action.hover_surface", Color("#303A4B"))
|
||||
pressed = active_theme.color("stage_panels.action.selected_surface", Color("#12394A"))
|
||||
border = secondary_border
|
||||
var border_width: int = active_theme.integer("popups.confirmation.button_border_width", 2, 1, 5)
|
||||
var radius: int = active_theme.integer("popups.confirmation.button_radius", 12, 8, 24)
|
||||
button.add_theme_stylebox_override("normal", _button_box(normal, border, border_width, radius))
|
||||
button.add_theme_stylebox_override("hover", _button_box(hover, border.lightened(0.12), border_width + 1, radius))
|
||||
button.add_theme_stylebox_override("pressed", _button_box(pressed, border, border_width + 1, radius))
|
||||
button.add_theme_stylebox_override("hover_pressed", _button_box(pressed.lightened(0.05), border, border_width + 1, radius))
|
||||
button.add_theme_stylebox_override("disabled", _button_box(secondary.darkened(0.18), secondary_border.darkened(0.35), 1, radius))
|
||||
button.add_theme_stylebox_override("focus", _button_box(normal, active_theme.color("popups.confirmation.focus_color", Color("#8BD9FF")), border_width + 1, radius))
|
||||
button.add_theme_color_override("font_color", label_color)
|
||||
button.add_theme_color_override("font_hover_color", label_color)
|
||||
button.add_theme_color_override("font_pressed_color", label_color)
|
||||
button.add_theme_color_override("font_disabled_color", Color("#AAB2C0"))
|
||||
button.add_theme_font_size_override("font_size", active_theme.integer("popups.confirmation.button_font_size", 20, 16, 32))
|
||||
var min_height: int = active_theme.integer("popups.confirmation.button_height", 52, 44, 80)
|
||||
var min_width: int = active_theme.integer("popups.confirmation.button_min_width", 112, 88, 240)
|
||||
button.custom_minimum_size = Vector2(maxf(button.custom_minimum_size.x, float(min_width)), maxf(button.custom_minimum_size.y, float(min_height)))
|
||||
if ui_font != null:
|
||||
button.add_theme_font_override("font", _strong_font(ui_font) if role == &"primary" else ui_font)
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
|
||||
|
||||
static func _install_dialog_surface(dialog: Window, stage_theme: SandboxTheme, ui_font: Font) -> void:
|
||||
dialog.set_meta(&"stage_dialog_theme", stage_theme)
|
||||
if ui_font != null:
|
||||
dialog.set_meta(&"stage_dialog_font", ui_font)
|
||||
dialog.theme = stage_theme.control_theme()
|
||||
dialog.borderless = true
|
||||
dialog.transparent = true
|
||||
dialog.transparent_bg = true
|
||||
dialog.unresizable = true
|
||||
dialog.wrap_controls = false
|
||||
dialog.add_theme_color_override("title_color", Color.TRANSPARENT)
|
||||
dialog.add_theme_font_size_override("title_font_size", 1)
|
||||
if ui_font != null:
|
||||
dialog.add_theme_font_override("font", ui_font)
|
||||
|
||||
var overlay_host: Node = _find_overlay_host(dialog)
|
||||
if overlay_host == null:
|
||||
return
|
||||
var overlay := Control.new()
|
||||
overlay.name = "StageDialogOverlay"
|
||||
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
overlay.z_index = 100
|
||||
overlay.z_as_relative = false
|
||||
overlay.visible = false
|
||||
overlay.gui_input.connect(_on_overlay_input.bind(dialog))
|
||||
overlay_host.add_child(overlay)
|
||||
dialog.set_meta(OVERLAY_META, overlay)
|
||||
dialog.set_meta(OPEN_META, false)
|
||||
dialog.set_meta(&"stage_dialog_overlay_host", overlay_host)
|
||||
dialog.tree_exiting.connect(_on_dialog_tree_exiting.bind(dialog, overlay), CONNECT_ONE_SHOT)
|
||||
if dialog.has_signal("confirmed"):
|
||||
dialog.connect("confirmed", _on_native_dialog_closed.bind(dialog))
|
||||
if dialog.has_signal("canceled"):
|
||||
dialog.connect("canceled", _on_native_dialog_closed.bind(dialog))
|
||||
if dialog.has_signal("custom_action"):
|
||||
dialog.connect("custom_action", _on_native_dialog_custom_action.bind(dialog))
|
||||
if dialog.has_signal("close_requested"):
|
||||
dialog.connect("close_requested", _on_native_dialog_closed.bind(dialog))
|
||||
|
||||
var backdrop := ColorRect.new()
|
||||
backdrop.name = "Backdrop"
|
||||
backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
backdrop.color = stage_theme.color("popups.confirmation.backdrop_color", Color("#080D15B8"))
|
||||
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
overlay.add_child(backdrop)
|
||||
|
||||
var center := CenterContainer.new()
|
||||
center.name = "CardCenter"
|
||||
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
overlay.add_child(center)
|
||||
|
||||
var card := PanelContainer.new()
|
||||
card.name = "DialogCard"
|
||||
card.custom_minimum_size = Vector2(560.0, 0.0)
|
||||
card.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
card.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
card.add_theme_stylebox_override("panel", panel_style(stage_theme))
|
||||
center.add_child(card)
|
||||
dialog.set_meta(CARD_META, card)
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.name = "DialogContent"
|
||||
content.add_theme_constant_override("separation", stage_theme.integer("popups.confirmation.body_gap", 12, 4, 24))
|
||||
card.add_child(content)
|
||||
|
||||
var header := HBoxContainer.new()
|
||||
header.name = "DialogHeader"
|
||||
header.add_theme_constant_override("separation", 12)
|
||||
content.add_child(header)
|
||||
|
||||
var icon_badge := PanelContainer.new()
|
||||
icon_badge.name = "IconBadge"
|
||||
icon_badge.custom_minimum_size = Vector2(40.0, 40.0)
|
||||
icon_badge.add_theme_stylebox_override("panel", _icon_box(Color("#173A4B"), Color("#4DB9FF")))
|
||||
header.add_child(icon_badge)
|
||||
dialog.set_meta(ICON_BADGE_META, icon_badge)
|
||||
|
||||
var icon_label := Label.new()
|
||||
icon_label.name = "DialogIcon"
|
||||
icon_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
icon_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
icon_label.add_theme_font_size_override("font_size", 22)
|
||||
icon_badge.add_child(icon_label)
|
||||
dialog.set_meta(ICON_LABEL_META, icon_label)
|
||||
|
||||
var title_label := Label.new()
|
||||
title_label.name = "DialogTitle"
|
||||
title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
title_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
title_label.add_theme_font_size_override("font_size", stage_theme.integer("popups.confirmation.title_font_size", 28, 20, 40))
|
||||
if ui_font != null:
|
||||
title_label.add_theme_font_override("font", _strong_font(ui_font))
|
||||
header.add_child(title_label)
|
||||
dialog.set_meta(TITLE_META, title_label)
|
||||
|
||||
var body := VBoxContainer.new()
|
||||
body.name = "DialogBody"
|
||||
body.add_theme_constant_override("separation", 10)
|
||||
body.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
var body_scroll := ScrollContainer.new()
|
||||
body_scroll.name = "DialogBodyScroll"
|
||||
body_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
body_scroll.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||
body_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
content.add_child(body_scroll)
|
||||
body_scroll.add_child(body)
|
||||
dialog.set_meta(BODY_META, body)
|
||||
dialog.set_meta(BODY_SCROLL_META, body_scroll)
|
||||
|
||||
var body_label := Label.new()
|
||||
body_label.name = "DialogMessage"
|
||||
body_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
body_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
body_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
body_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
body_label.add_theme_font_size_override("font_size", stage_theme.integer("popups.confirmation.message_font_size", 20, 16, 32))
|
||||
body.add_child(body_label)
|
||||
dialog.set_meta(&"stage_dialog_message", body_label)
|
||||
|
||||
var detail_box := PanelContainer.new()
|
||||
detail_box.name = "DialogDetails"
|
||||
detail_box.add_theme_stylebox_override("panel", _detail_box(stage_theme))
|
||||
var detail_label := Label.new()
|
||||
detail_label.name = "DetailsText"
|
||||
detail_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
detail_label.add_theme_font_size_override("font_size", stage_theme.integer("popups.confirmation.message_font_size", 20, 16, 32))
|
||||
detail_box.add_child(detail_label)
|
||||
body.add_child(detail_box)
|
||||
dialog.set_meta(DETAIL_META, detail_box)
|
||||
|
||||
var note := Label.new()
|
||||
note.name = "DialogNote"
|
||||
note.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
note.add_theme_color_override("font_color", Color("#B8C7D8"))
|
||||
note.add_theme_font_size_override("font_size", 17)
|
||||
body.add_child(note)
|
||||
dialog.set_meta(NOTE_META, note)
|
||||
|
||||
var action_row := HBoxContainer.new()
|
||||
action_row.name = "DialogActions"
|
||||
action_row.alignment = BoxContainer.ALIGNMENT_END
|
||||
action_row.add_theme_constant_override("separation", stage_theme.integer("popups.confirmation.footer_gap", 12, 8, 24))
|
||||
content.add_child(action_row)
|
||||
dialog.set_meta(ACTIONS_META, action_row)
|
||||
|
||||
var extras: Array[Control] = []
|
||||
var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null
|
||||
var cancel_button: Button = (dialog as ConfirmationDialog).get_cancel_button() if dialog is ConfirmationDialog else null
|
||||
var default_label: Label = (dialog as AcceptDialog).get_label() if dialog is AcceptDialog else null
|
||||
if default_label != null:
|
||||
default_label.visible = false
|
||||
if ok_button != null:
|
||||
ok_button.visible = false
|
||||
if cancel_button != null:
|
||||
cancel_button.visible = false
|
||||
for child: Node in dialog.get_children():
|
||||
if child is Control and not child is Button and child != default_label and child != ok_button and child != cancel_button:
|
||||
var control: Control = child as Control
|
||||
control.reparent(body)
|
||||
extras.append(control)
|
||||
dialog.set_meta(EXTRAS_META, extras)
|
||||
dialog.hide()
|
||||
|
||||
|
||||
static func _refresh_dialog_surface(
|
||||
dialog: Window,
|
||||
stage_theme: SandboxTheme,
|
||||
ui_font: Font,
|
||||
primary_button: Button = null,
|
||||
destructive_buttons: Array = []
|
||||
) -> void:
|
||||
if not dialog.has_meta(OVERLAY_META):
|
||||
return
|
||||
dialog.set_meta(&"stage_dialog_theme", stage_theme)
|
||||
if ui_font != null:
|
||||
dialog.set_meta(&"stage_dialog_font", ui_font)
|
||||
var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null
|
||||
var cancel_button: Button = (dialog as ConfirmationDialog).get_cancel_button() if dialog is ConfirmationDialog else null
|
||||
var original_message: String = String((dialog as AcceptDialog).dialog_text) if dialog is AcceptDialog else ""
|
||||
var body_label: Label = dialog.get_meta(&"stage_dialog_message") as Label
|
||||
var detail_box: PanelContainer = dialog.get_meta(DETAIL_META) as PanelContainer
|
||||
var detail_label: Label = detail_box.get_child(0) as Label if detail_box != null and detail_box.get_child_count() > 0 else null
|
||||
var note_label: Label = dialog.get_meta(NOTE_META) as Label
|
||||
var body_text: String = original_message
|
||||
var note_text: String = ""
|
||||
var note_marker: int = body_text.find("\n\nℹ️")
|
||||
if note_marker >= 0:
|
||||
note_text = body_text.substr(note_marker + 2)
|
||||
body_text = body_text.substr(0, note_marker)
|
||||
var detail_marker: int = body_text.find("\n\n•")
|
||||
var detail_text: String = ""
|
||||
if detail_marker >= 0:
|
||||
detail_text = body_text.substr(detail_marker + 2)
|
||||
body_text = body_text.substr(0, detail_marker)
|
||||
if body_label != null:
|
||||
body_label.text = body_text
|
||||
body_label.visible = not body_text.is_empty()
|
||||
if detail_label != null and detail_box != null:
|
||||
detail_label.text = detail_text
|
||||
detail_box.visible = not detail_text.is_empty()
|
||||
if note_label != null:
|
||||
note_label.text = note_text
|
||||
note_label.visible = not note_text.is_empty()
|
||||
var heading: Dictionary = _split_heading(dialog.title, String(dialog.get_meta(ICON_META, _icon_for_dialog(dialog))))
|
||||
var title_label: Label = dialog.get_meta(TITLE_META) as Label
|
||||
var icon_label: Label = dialog.get_meta(ICON_LABEL_META) as Label
|
||||
if title_label != null:
|
||||
title_label.text = String(heading.get("title", dialog.title))
|
||||
var font_size: int = stage_theme.integer("popups.confirmation.title_font_size", 28, 20, 40)
|
||||
title_label.add_theme_font_size_override("font_size", font_size)
|
||||
title_label.add_theme_color_override("font_color", stage_theme.color("global.text_color", Color("#F5F7FA")))
|
||||
if ui_font != null:
|
||||
title_label.add_theme_font_override("font", _strong_font(ui_font))
|
||||
if icon_label != null:
|
||||
icon_label.text = String(heading.get("icon", "❔"))
|
||||
var icon_badge: PanelContainer = dialog.get_meta(ICON_BADGE_META) as PanelContainer
|
||||
if icon_badge != null:
|
||||
var heading_title: String = String(heading.get("title", "")).to_lower()
|
||||
var badge_background: Color = Color("#173A4B")
|
||||
var badge_border: Color = Color("#4DB9FF")
|
||||
if String(heading.get("icon", "")).contains("⚠"):
|
||||
badge_background = Color("#493A13")
|
||||
badge_border = Color("#D7AE28")
|
||||
elif String(heading.get("icon", "")).contains("🗑") or "erase" in heading_title or "discard" in heading_title:
|
||||
badge_background = Color("#48262D")
|
||||
badge_border = Color("#E66B75")
|
||||
icon_badge.add_theme_stylebox_override("panel", _icon_box(badge_background, badge_border))
|
||||
var text_color: Color = stage_theme.color("global.text_color", Color("#F5F7FA"))
|
||||
if body_label != null:
|
||||
body_label.add_theme_color_override("font_color", text_color)
|
||||
if detail_label != null:
|
||||
detail_label.add_theme_color_override("font_color", text_color)
|
||||
var extras: Array = dialog.get_meta(EXTRAS_META, []) as Array
|
||||
for extra: Variant in extras:
|
||||
if extra is Control:
|
||||
_apply_control_font(extra as Control, ui_font, stage_theme.integer("popups.confirmation.message_font_size", 20, 16, 32))
|
||||
|
||||
var action_row: HBoxContainer = dialog.get_meta(ACTIONS_META) as HBoxContainer
|
||||
if action_row != null:
|
||||
for old_button: Node in action_row.get_children():
|
||||
action_row.remove_child(old_button)
|
||||
old_button.queue_free()
|
||||
var primary: Button = primary_button if primary_button != null else ok_button
|
||||
if ok_button != null:
|
||||
ok_button.visible = false
|
||||
if cancel_button != null:
|
||||
cancel_button.visible = false
|
||||
for raw_button: Variant in destructive_buttons:
|
||||
if raw_button is Button:
|
||||
(raw_button as Button).visible = false
|
||||
var ordered: Array[Button] = []
|
||||
_append_unique_button(ordered, primary)
|
||||
for raw_destructive: Variant in destructive_buttons:
|
||||
if raw_destructive is Button:
|
||||
_append_unique_button(ordered, raw_destructive as Button)
|
||||
_append_unique_button(ordered, cancel_button)
|
||||
_append_unique_button(ordered, ok_button)
|
||||
for original: Button in ordered:
|
||||
var visual: Button = Button.new()
|
||||
visual.text = original.text
|
||||
visual.name = "Action_%s" % String(original.name)
|
||||
visual.size_flags_horizontal = Control.SIZE_SHRINK_END
|
||||
var role: StringName = &"secondary"
|
||||
if original == primary:
|
||||
role = &"primary"
|
||||
elif _contains_button(destructive_buttons, original):
|
||||
role = &"danger"
|
||||
style_button(visual, role, stage_theme, ui_font)
|
||||
visual.pressed.connect(func() -> void: original.pressed.emit())
|
||||
visual.set_meta(&"stage_dialog_original_button", original)
|
||||
original.set_meta(&"stage_dialog_visual_button", visual)
|
||||
if action_row != null:
|
||||
action_row.add_child(visual)
|
||||
|
||||
|
||||
static func _on_overlay_input(event: InputEvent, dialog: Window) -> void:
|
||||
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_ESCAPE:
|
||||
dismiss_on_escape(dialog)
|
||||
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
|
||||
if overlay != null and overlay.get_viewport() != null:
|
||||
overlay.get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
static func _fit_body_scroll(dialog: Window, viewport_size: Vector2i, stage_theme: SandboxTheme) -> void:
|
||||
var body_scroll: ScrollContainer = dialog.get_meta(BODY_SCROLL_META) as ScrollContainer
|
||||
var body: Control = dialog.get_meta(BODY_META) as Control
|
||||
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
|
||||
var header: Control = overlay.get_node_or_null("CardCenter/DialogCard/DialogContent/DialogHeader") as Control if overlay != null else null
|
||||
var actions: Control = dialog.get_meta(ACTIONS_META) as Control
|
||||
var card: Control = dialog.get_meta(CARD_META) as Control
|
||||
if body_scroll == null or body == null or header == null or actions == null or card == null:
|
||||
return
|
||||
var card_width: float = card.custom_minimum_size.x
|
||||
var card_padding: float = float(stage_theme.integer("popups.confirmation.padding", 22, 12, 36))
|
||||
var content_gap: float = float(stage_theme.integer("popups.confirmation.body_gap", 12, 4, 24))
|
||||
var body_width: float = maxf(0.0, card_width - card_padding * 2.0)
|
||||
var preferred_body_height: float = body.get_combined_minimum_size().y
|
||||
var fixed_height: float = header.get_combined_minimum_size().y + actions.get_combined_minimum_size().y + card_padding * 2.0 + content_gap * 2.0
|
||||
var max_body_height: float = maxf(48.0, float(viewport_size.y) - 36.0 - fixed_height)
|
||||
if preferred_body_height > max_body_height:
|
||||
var vertical_bar: VScrollBar = body_scroll.get_v_scroll_bar()
|
||||
if vertical_bar != null:
|
||||
body_width = maxf(0.0, body_width - vertical_bar.get_combined_minimum_size().x)
|
||||
body.custom_minimum_size.x = body_width
|
||||
body.size.x = body_width
|
||||
preferred_body_height = body.get_combined_minimum_size().y
|
||||
body_scroll.custom_minimum_size = Vector2(0.0, minf(preferred_body_height, max_body_height))
|
||||
|
||||
|
||||
static func _find_overlay_host(dialog: Window) -> Node:
|
||||
var ancestor: Node = dialog.get_parent()
|
||||
while ancestor != null:
|
||||
if ancestor is CanvasLayer:
|
||||
return ancestor
|
||||
ancestor = ancestor.get_parent()
|
||||
var tree: SceneTree = dialog.get_tree()
|
||||
var scene: Node = tree.current_scene if tree != null else null
|
||||
if scene != null:
|
||||
var ui_layer: Node = scene.get_node_or_null("UI")
|
||||
if ui_layer is CanvasLayer:
|
||||
return ui_layer
|
||||
var existing: Node = scene.get_node_or_null("StageDialogLayer")
|
||||
if existing is CanvasLayer:
|
||||
return existing
|
||||
var layer := CanvasLayer.new()
|
||||
layer.name = "StageDialogLayer"
|
||||
layer.layer = 100
|
||||
scene.add_child(layer)
|
||||
return layer
|
||||
return null
|
||||
|
||||
|
||||
static func _on_native_dialog_closed(dialog: Window) -> void:
|
||||
hide(dialog)
|
||||
|
||||
|
||||
static func _on_native_dialog_custom_action(_action: String, dialog: Window) -> void:
|
||||
hide(dialog)
|
||||
|
||||
|
||||
static func _on_dialog_tree_exiting(dialog: Window, overlay: Control) -> void:
|
||||
if dialog != null and is_instance_valid(dialog):
|
||||
dialog.set_meta(OPEN_META, false)
|
||||
if overlay != null and is_instance_valid(overlay):
|
||||
overlay.queue_free()
|
||||
|
||||
|
||||
static func _append_unique_button(buttons: Array[Button], button: Button) -> void:
|
||||
if button != null and not buttons.has(button):
|
||||
buttons.append(button)
|
||||
|
||||
|
||||
static func _contains_button(buttons: Array, button: Button) -> bool:
|
||||
for item: Variant in buttons:
|
||||
if item is Button and item == button:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
static func _split_heading(raw_title: String, fallback_icon: String) -> Dictionary:
|
||||
var icon: String = fallback_icon
|
||||
var title: String = raw_title
|
||||
var space: int = raw_title.find(" ")
|
||||
if space > 0 and space < 5:
|
||||
var possible_icon: String = raw_title.substr(0, space)
|
||||
if possible_icon.length() <= 4 and not possible_icon.is_empty():
|
||||
icon = possible_icon
|
||||
title = raw_title.substr(space + 1).strip_edges()
|
||||
if icon.is_empty():
|
||||
icon = "❔"
|
||||
return {"icon": icon, "title": title}
|
||||
|
||||
|
||||
static func _icon_for_dialog(dialog: Window) -> String:
|
||||
var dialog_name: String = dialog.name.to_lower()
|
||||
if "artwork" in dialog_name or "scenery" in dialog_name:
|
||||
return "🖼️"
|
||||
if "asset" in dialog_name or "error" in dialog_name or "warning" in dialog_name:
|
||||
return "⚠️"
|
||||
if "speech" in dialog_name or "speak" in dialog_name:
|
||||
return "🗣️"
|
||||
if "rename" in dialog_name:
|
||||
return "📝"
|
||||
return "❔"
|
||||
|
||||
|
||||
static func _theme_for(dialog: Window) -> SandboxTheme:
|
||||
var raw: Variant = dialog.get_meta(&"stage_dialog_theme", null)
|
||||
return raw as SandboxTheme if raw is SandboxTheme else THEME_DEFAULTS.new()
|
||||
|
||||
|
||||
static func _apply_control_font(control: Control, ui_font: Font, size: int) -> void:
|
||||
control.add_theme_font_size_override("font_size", size)
|
||||
if ui_font != null:
|
||||
control.add_theme_font_override("font", ui_font)
|
||||
for child: Node in control.get_children():
|
||||
if child is Control:
|
||||
_apply_control_font(child as Control, ui_font, size)
|
||||
|
||||
|
||||
static func _icon_box(background: Color, border: Color) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = background
|
||||
style.border_color = border
|
||||
style.set_border_width_all(1)
|
||||
style.set_corner_radius_all(9)
|
||||
return style
|
||||
|
||||
|
||||
static func _detail_box(stage_theme: SandboxTheme) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color("#151C28")
|
||||
style.border_color = Color("#39465B")
|
||||
style.set_border_width_all(1)
|
||||
style.set_corner_radius_all(9)
|
||||
style.content_margin_left = 12.0
|
||||
style.content_margin_right = 12.0
|
||||
style.content_margin_top = 8.0
|
||||
style.content_margin_bottom = 8.0
|
||||
return style
|
||||
|
||||
|
||||
static func _button_box(background: Color, border: Color, width: int, radius: int) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = background
|
||||
style.border_color = border
|
||||
style.set_border_width_all(width)
|
||||
style.set_corner_radius_all(radius)
|
||||
style.content_margin_left = 12.0
|
||||
style.content_margin_right = 12.0
|
||||
style.content_margin_top = 8.0
|
||||
style.content_margin_bottom = 8.0
|
||||
return style
|
||||
|
||||
|
||||
static func _strong_font(base_font: Font) -> Font:
|
||||
if base_font == null:
|
||||
return ThemeDB.fallback_font
|
||||
var strong_font := FontVariation.new()
|
||||
strong_font.base_font = base_font
|
||||
strong_font.variation_embolden = 0.55
|
||||
return strong_font
|
||||
@@ -0,0 +1 @@
|
||||
uid://6iflwn2v1lkg
|
||||
Reference in New Issue
Block a user