feature updates
This commit is contained in:
347
scripts/ui/ChoiceModal.gd
Normal file
347
scripts/ui/ChoiceModal.gd
Normal file
@@ -0,0 +1,347 @@
|
||||
class_name ChoiceModal
|
||||
extends CanvasLayer
|
||||
|
||||
## ChoiceModal - UI component for multi-modal ability choices
|
||||
## Displays options like "Select 1 of 3 following actions" and returns selection
|
||||
|
||||
signal choice_made(selected_indices: Array)
|
||||
signal choice_cancelled
|
||||
|
||||
# UI Elements
|
||||
var backdrop: ColorRect
|
||||
var modal_panel: Panel
|
||||
var title_label: Label
|
||||
var options_container: VBoxContainer
|
||||
var confirm_button: Button
|
||||
var cancel_button: Button
|
||||
|
||||
# State
|
||||
var _modes: Array = []
|
||||
var _select_count: int = 1
|
||||
var _select_up_to: bool = false
|
||||
var _selected_indices: Array = []
|
||||
var _cancellable: bool = false
|
||||
var _option_buttons: Array = []
|
||||
|
||||
# Cached styles for option buttons (created once, reused)
|
||||
var _option_normal_style: StyleBoxFlat
|
||||
var _option_selected_style: StyleBoxFlat
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
layer = 200 # High z-index for modal overlay
|
||||
_create_cached_styles()
|
||||
_create_ui()
|
||||
visible = false
|
||||
|
||||
|
||||
func _create_cached_styles() -> void:
|
||||
# Normal button style
|
||||
_option_normal_style = StyleBoxFlat.new()
|
||||
_option_normal_style.bg_color = Color(0.15, 0.15, 0.2, 0.9)
|
||||
_option_normal_style.set_border_width_all(1)
|
||||
_option_normal_style.border_color = Color(0.3, 0.3, 0.4)
|
||||
_option_normal_style.set_corner_radius_all(4)
|
||||
_option_normal_style.set_content_margin_all(10)
|
||||
|
||||
# Selected button style (gold highlight)
|
||||
_option_selected_style = StyleBoxFlat.new()
|
||||
_option_selected_style.bg_color = Color(0.25, 0.22, 0.15, 0.95)
|
||||
_option_selected_style.border_color = Color(0.7, 0.55, 0.2)
|
||||
_option_selected_style.set_border_width_all(2)
|
||||
_option_selected_style.set_corner_radius_all(4)
|
||||
_option_selected_style.set_content_margin_all(10)
|
||||
|
||||
|
||||
func _create_ui() -> void:
|
||||
# Backdrop - semi-transparent dark overlay
|
||||
backdrop = ColorRect.new()
|
||||
add_child(backdrop)
|
||||
backdrop.color = Color(0, 0, 0, 0.7)
|
||||
backdrop.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
backdrop.gui_input.connect(_on_backdrop_input)
|
||||
|
||||
# Center container for modal
|
||||
var center = CenterContainer.new()
|
||||
add_child(center)
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
# Modal panel
|
||||
modal_panel = Panel.new()
|
||||
center.add_child(modal_panel)
|
||||
modal_panel.custom_minimum_size = Vector2(500, 200)
|
||||
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.08, 0.08, 0.12, 0.98)
|
||||
style.border_color = Color(0.5, 0.4, 0.2) # Gold border
|
||||
style.set_border_width_all(2)
|
||||
style.set_corner_radius_all(8)
|
||||
style.set_content_margin_all(20)
|
||||
modal_panel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
# Main vertical layout
|
||||
var vbox = VBoxContainer.new()
|
||||
modal_panel.add_child(vbox)
|
||||
vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
vbox.offset_left = 20
|
||||
vbox.offset_right = -20
|
||||
vbox.offset_top = 20
|
||||
vbox.offset_bottom = -20
|
||||
vbox.add_theme_constant_override("separation", 15)
|
||||
|
||||
# Title
|
||||
title_label = Label.new()
|
||||
vbox.add_child(title_label)
|
||||
title_label.text = "Select an action:"
|
||||
title_label.add_theme_font_size_override("font_size", 20)
|
||||
title_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
|
||||
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
|
||||
# Options container
|
||||
options_container = VBoxContainer.new()
|
||||
vbox.add_child(options_container)
|
||||
options_container.add_theme_constant_override("separation", 8)
|
||||
options_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
|
||||
# Button row
|
||||
var button_row = HBoxContainer.new()
|
||||
vbox.add_child(button_row)
|
||||
button_row.add_theme_constant_override("separation", 15)
|
||||
button_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
|
||||
# Confirm button (for multi-select)
|
||||
confirm_button = _create_button("Confirm", Color(0.2, 0.5, 0.3))
|
||||
button_row.add_child(confirm_button)
|
||||
confirm_button.pressed.connect(_on_confirm_pressed)
|
||||
confirm_button.visible = false # Only shown for multi-select
|
||||
|
||||
# Cancel button
|
||||
cancel_button = _create_button("Cancel", Color(0.5, 0.3, 0.3))
|
||||
button_row.add_child(cancel_button)
|
||||
cancel_button.pressed.connect(_on_cancel_pressed)
|
||||
cancel_button.visible = false # Only shown if cancellable
|
||||
|
||||
|
||||
func _create_button(text: String, base_color: Color) -> Button:
|
||||
var button = Button.new()
|
||||
button.text = text
|
||||
button.custom_minimum_size = Vector2(100, 40)
|
||||
|
||||
var normal_style = StyleBoxFlat.new()
|
||||
normal_style.bg_color = base_color
|
||||
normal_style.set_border_width_all(1)
|
||||
normal_style.border_color = base_color.lightened(0.3)
|
||||
normal_style.set_corner_radius_all(4)
|
||||
normal_style.set_content_margin_all(8)
|
||||
button.add_theme_stylebox_override("normal", normal_style)
|
||||
|
||||
var hover_style = normal_style.duplicate()
|
||||
hover_style.bg_color = base_color.lightened(0.15)
|
||||
button.add_theme_stylebox_override("hover", hover_style)
|
||||
|
||||
var pressed_style = normal_style.duplicate()
|
||||
pressed_style.bg_color = base_color.darkened(0.1)
|
||||
button.add_theme_stylebox_override("pressed", pressed_style)
|
||||
|
||||
return button
|
||||
|
||||
|
||||
func _create_option_button(index: int, description: String) -> Button:
|
||||
var button = Button.new()
|
||||
button.text = str(index + 1) + ". " + description
|
||||
button.custom_minimum_size = Vector2(460, 50)
|
||||
button.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
button.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING
|
||||
|
||||
# Normal state
|
||||
var normal_style = StyleBoxFlat.new()
|
||||
normal_style.bg_color = Color(0.15, 0.15, 0.2, 0.9)
|
||||
normal_style.set_border_width_all(1)
|
||||
normal_style.border_color = Color(0.3, 0.3, 0.4)
|
||||
normal_style.set_corner_radius_all(4)
|
||||
normal_style.set_content_margin_all(10)
|
||||
button.add_theme_stylebox_override("normal", normal_style)
|
||||
|
||||
# Hover state
|
||||
var hover_style = normal_style.duplicate()
|
||||
hover_style.bg_color = Color(0.2, 0.2, 0.3, 0.95)
|
||||
hover_style.border_color = Color(0.5, 0.4, 0.2)
|
||||
button.add_theme_stylebox_override("hover", hover_style)
|
||||
|
||||
# Pressed/selected state (gold highlight)
|
||||
var pressed_style = normal_style.duplicate()
|
||||
pressed_style.bg_color = Color(0.25, 0.22, 0.15, 0.95)
|
||||
pressed_style.border_color = Color(0.7, 0.55, 0.2)
|
||||
pressed_style.set_border_width_all(2)
|
||||
button.add_theme_stylebox_override("pressed", pressed_style)
|
||||
|
||||
# Font settings
|
||||
button.add_theme_font_size_override("font_size", 14)
|
||||
button.add_theme_color_override("font_color", Color(0.85, 0.85, 0.85))
|
||||
button.add_theme_color_override("font_hover_color", Color(1, 0.95, 0.8))
|
||||
|
||||
# Connect press signal
|
||||
button.pressed.connect(_on_option_pressed.bind(index))
|
||||
|
||||
return button
|
||||
|
||||
|
||||
## Show modal and await selection
|
||||
## Returns array of selected mode indices
|
||||
func show_choices(
|
||||
title: String,
|
||||
modes: Array,
|
||||
select_count: int = 1,
|
||||
select_up_to: bool = false,
|
||||
cancellable: bool = false
|
||||
) -> Array:
|
||||
_modes = modes
|
||||
_select_count = select_count
|
||||
_select_up_to = select_up_to
|
||||
_cancellable = cancellable
|
||||
_selected_indices = []
|
||||
_option_buttons = []
|
||||
|
||||
# Update title
|
||||
if select_up_to:
|
||||
title_label.text = "Select up to %d action%s:" % [select_count, "s" if select_count > 1 else ""]
|
||||
else:
|
||||
title_label.text = "Select %d action%s:" % [select_count, "s" if select_count > 1 else ""]
|
||||
|
||||
# Clear and populate options
|
||||
for child in options_container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
await get_tree().process_frame # Wait for queue_free
|
||||
|
||||
for i in range(modes.size()):
|
||||
var mode = modes[i]
|
||||
var description = mode.get("description", "Option " + str(i + 1))
|
||||
var button = _create_option_button(i, description)
|
||||
options_container.add_child(button)
|
||||
_option_buttons.append(button)
|
||||
|
||||
# Show confirm button only for multi-select
|
||||
confirm_button.visible = (select_count > 1 or select_up_to)
|
||||
_update_confirm_button()
|
||||
|
||||
# Show cancel if cancellable
|
||||
cancel_button.visible = cancellable
|
||||
|
||||
# Resize panel to fit content
|
||||
await get_tree().process_frame
|
||||
var content_height = 20 + 30 + 15 + (modes.size() * 58) + 15 + 50 + 20
|
||||
modal_panel.custom_minimum_size = Vector2(500, min(content_height, 600))
|
||||
|
||||
visible = true
|
||||
|
||||
# Wait for selection
|
||||
var result = await _wait_for_selection()
|
||||
visible = false
|
||||
return result
|
||||
|
||||
|
||||
## Internal: Wait for user selection using a callback pattern
|
||||
func _wait_for_selection() -> Array:
|
||||
var result: Array = []
|
||||
|
||||
# Create a one-shot signal connection
|
||||
var completed = false
|
||||
|
||||
var on_choice = func(indices: Array):
|
||||
result = indices
|
||||
completed = true
|
||||
|
||||
var on_cancel = func():
|
||||
result = []
|
||||
completed = true
|
||||
|
||||
choice_made.connect(on_choice, CONNECT_ONE_SHOT)
|
||||
choice_cancelled.connect(on_cancel, CONNECT_ONE_SHOT)
|
||||
|
||||
# Wait until completed
|
||||
while not completed:
|
||||
await get_tree().process_frame
|
||||
|
||||
return result
|
||||
|
||||
|
||||
func _on_option_pressed(index: int) -> void:
|
||||
if _select_count == 1 and not _select_up_to:
|
||||
# Single select - immediately return
|
||||
choice_made.emit([index])
|
||||
return
|
||||
|
||||
# Multi-select - toggle selection
|
||||
if index in _selected_indices:
|
||||
_selected_indices.erase(index)
|
||||
else:
|
||||
if _selected_indices.size() < _select_count:
|
||||
_selected_indices.append(index)
|
||||
|
||||
_update_option_visuals()
|
||||
_update_confirm_button()
|
||||
|
||||
|
||||
func _update_option_visuals() -> void:
|
||||
for i in range(_option_buttons.size()):
|
||||
var button = _option_buttons[i] as Button
|
||||
var is_selected = i in _selected_indices
|
||||
|
||||
# Use cached styles instead of creating new ones each time
|
||||
if is_selected:
|
||||
button.add_theme_stylebox_override("normal", _option_selected_style)
|
||||
else:
|
||||
button.add_theme_stylebox_override("normal", _option_normal_style)
|
||||
|
||||
|
||||
func _update_confirm_button() -> void:
|
||||
if _select_up_to:
|
||||
confirm_button.disabled = false
|
||||
confirm_button.text = "Confirm (%d)" % _selected_indices.size()
|
||||
else:
|
||||
confirm_button.disabled = _selected_indices.size() != _select_count
|
||||
confirm_button.text = "Confirm (%d/%d)" % [_selected_indices.size(), _select_count]
|
||||
|
||||
|
||||
func _on_confirm_pressed() -> void:
|
||||
if _select_up_to or _selected_indices.size() == _select_count:
|
||||
choice_made.emit(_selected_indices.duplicate())
|
||||
|
||||
|
||||
func _on_cancel_pressed() -> void:
|
||||
choice_cancelled.emit()
|
||||
|
||||
|
||||
func _on_backdrop_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed:
|
||||
if _cancellable:
|
||||
choice_cancelled.emit()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not visible:
|
||||
return
|
||||
|
||||
# Keyboard shortcuts
|
||||
if event is InputEventKey and event.pressed:
|
||||
# Number keys 1-9 for quick selection
|
||||
if event.keycode >= KEY_1 and event.keycode <= KEY_9:
|
||||
var index = event.keycode - KEY_1
|
||||
if index < _modes.size():
|
||||
_on_option_pressed(index)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# Enter to confirm (multi-select only)
|
||||
elif event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER:
|
||||
if confirm_button.visible and not confirm_button.disabled:
|
||||
_on_confirm_pressed()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# Escape to cancel
|
||||
elif event.keycode == KEY_ESCAPE:
|
||||
if _cancellable:
|
||||
choice_cancelled.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
Reference in New Issue
Block a user