netfishing/ui/controller_mapping_panel.gd

566 lines
17 KiB
GDScript

class_name ControllerMappingPanel
extends Control
signal closed
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
const CAPTURE_NEUTRAL_SECONDS: float = 0.22
const CANCEL_COMBO_HOLD_SECONDS: float = 1.25
var _mapping_manager: ControllerMappingManagerType
var _binding_buttons: Dictionary = {}
var _controller_label: Label
var _instruction_label: Label
var _progress_label: Label
var _auto_map_button: Button
var _reset_button: Button
var _close_button: Button
var _capturing_role: StringName = &""
var _auto_map_active: bool = false
var _auto_map_index: int = -1
var _auto_map_draft: Dictionary = {}
var _capture_device_id: int = 0
var _waiting_for_neutral: bool = false
var _neutral_elapsed: float = 0.0
var _cancel_left_bumper_pressed: bool = false
var _cancel_right_bumper_pressed: bool = false
var _cancel_combo_elapsed: float = 0.0
func _ready() -> void:
set_process_input(true)
set_process(true)
_build_interface()
hide()
func setup(mapping_manager: ControllerMappingManagerType) -> void:
_mapping_manager = mapping_manager
if not _mapping_manager.active_controller_changed.is_connected(
_on_active_controller_changed
):
_mapping_manager.active_controller_changed.connect(
_on_active_controller_changed
)
if not _mapping_manager.active_profile_changed.is_connected(
_refresh_bindings
):
_mapping_manager.active_profile_changed.connect(_refresh_bindings)
if not _mapping_manager.controller_input_observed.is_connected(
_on_controller_input_observed
):
_mapping_manager.controller_input_observed.connect(
_on_controller_input_observed
)
_refresh_bindings()
func open_panel() -> void:
if _mapping_manager == null:
return
_cancel_capture()
_mapping_manager.recalibrate_active_device()
show()
_refresh_bindings()
_auto_map_button.grab_focus()
func is_open() -> bool:
return visible
func is_capturing() -> bool:
return visible and not _capturing_role.is_empty()
func request_back() -> void:
if not visible:
return
if not _capturing_role.is_empty():
_cancel_capture()
return
close_panel()
func close_panel() -> void:
_cancel_capture()
hide()
closed.emit()
func _input(event: InputEvent) -> void:
if not visible or _mapping_manager == null:
return
if _capturing_role.is_empty():
return
var key_event := event as InputEventKey
if (
key_event != null
and key_event.pressed
and not key_event.echo
and key_event.keycode == KEY_ESCAPE
):
get_viewport().set_input_as_handled()
_cancel_capture()
_progress_label.text = "controller mapping cancelled"
return
func _process(delta: float) -> void:
if (
not visible
or _mapping_manager == null
or _capturing_role.is_empty()
):
return
if _cancel_left_bumper_pressed and _cancel_right_bumper_pressed:
_cancel_combo_elapsed += delta
_progress_label.text = "keep holding both bumpers to cancel"
if _cancel_combo_elapsed >= CANCEL_COMBO_HOLD_SECONDS:
_cancel_capture()
_progress_label.text = "controller mapping cancelled"
return
_cancel_combo_elapsed = 0.0
if _waiting_for_neutral:
if not _mapping_manager.are_capture_inputs_neutral(_capture_device_id):
_neutral_elapsed = 0.0
return
_neutral_elapsed += delta
if _neutral_elapsed < CAPTURE_NEUTRAL_SECONDS:
return
_waiting_for_neutral = false
_neutral_elapsed = 0.0
_refresh_capture_prompt()
return
var pressed_button: int = _mapping_manager.get_pressed_capture_button(
_capture_device_id
)
if pressed_button < 0:
return
var button_event := InputEventJoypadButton.new()
button_event.device = _capture_device_id
button_event.button_index = pressed_button as JoyButton
button_event.pressed = true
_try_capture_event(button_event)
func _on_controller_input_observed(event: InputEvent) -> void:
if not visible or _mapping_manager == null:
return
if _capturing_role.is_empty():
return
get_viewport().set_input_as_handled()
var button_event := event as InputEventJoypadButton
var motion_event := event as InputEventJoypadMotion
var event_device: int = -1
if button_event != null:
event_device = button_event.device
elif motion_event != null:
event_device = motion_event.device
else:
return
if event_device != _capture_device_id:
return
_track_cancel_combo_button(button_event)
if _waiting_for_neutral:
return
_try_capture_event(event)
func _try_capture_event(event: InputEvent) -> void:
var binding: Dictionary = _mapping_manager.binding_from_event(
_capturing_role,
event,
)
if binding.is_empty():
return
if not _mapping_manager.validate_binding(_capturing_role, binding):
return
_accept_captured_binding(binding)
func _build_interface() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_STOP
z_index = 100
UtilityPageStyleType.apply_page(self)
var frame := PanelContainer.new()
frame.set_anchors_preset(Control.PRESET_FULL_RECT)
frame.offset_left = 18.0
frame.offset_top = 18.0
frame.offset_right = -18.0
frame.offset_bottom = -18.0
frame.add_theme_stylebox_override(
"panel",
UtilityPageStyleType.rounded_style(
UtilityPageStyleType.OCEAN_PANEL_DEEP,
20,
),
)
add_child(frame)
var outer_margin := MarginContainer.new()
outer_margin.add_theme_constant_override("margin_left", 24)
outer_margin.add_theme_constant_override("margin_top", 20)
outer_margin.add_theme_constant_override("margin_right", 24)
outer_margin.add_theme_constant_override("margin_bottom", 20)
frame.add_child(outer_margin)
var page := VBoxContainer.new()
page.add_theme_constant_override("separation", 12)
outer_margin.add_child(page)
var heading_row := HBoxContainer.new()
heading_row.add_theme_constant_override("separation", 12)
page.add_child(heading_row)
var title := Label.new()
title.text = "controller mapping"
title.add_theme_font_size_override("font_size", 28)
title.add_theme_color_override(
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
heading_row.add_child(title)
_controller_label = Label.new()
_controller_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_controller_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_controller_label.add_theme_font_size_override("font_size", 15)
_controller_label.add_theme_color_override(
"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
)
heading_row.add_child(_controller_label)
_instruction_label = Label.new()
_instruction_label.text = (
"auto-map walks through NETfishing's controller actions. "
+ "select any row afterward to override it; the mapped back "
+ "control cancels capture."
)
_instruction_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_instruction_label.add_theme_font_size_override("font_size", 15)
_instruction_label.add_theme_color_override(
"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
)
page.add_child(_instruction_label)
_progress_label = Label.new()
_progress_label.custom_minimum_size.y = 34.0
_progress_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_progress_label.add_theme_font_size_override("font_size", 18)
_progress_label.add_theme_color_override(
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
)
page.add_child(_progress_label)
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
page.add_child(scroll)
var role_grid := GridContainer.new()
role_grid.columns = 2
role_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
role_grid.add_theme_constant_override("h_separation", 12)
role_grid.add_theme_constant_override("v_separation", 7)
scroll.add_child(role_grid)
for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
var role_label := Label.new()
role_label.text = ControllerMappingManagerType.ROLE_LABELS.get(
role,
str(role),
)
role_label.custom_minimum_size = Vector2(330.0, 42.0)
role_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
role_label.add_theme_font_size_override("font_size", 16)
role_label.add_theme_color_override(
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
)
role_grid.add_child(role_label)
var binding_button := Button.new()
binding_button.custom_minimum_size = Vector2(210.0, 42.0)
binding_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
binding_button.focus_mode = Control.FOCUS_ALL
binding_button.tooltip_text = (
"change " + str(ControllerMappingManagerType.ROLE_LABELS.get(
role,
str(role),
))
)
UtilityPageStyleType.apply_compact_ocean_button(binding_button)
binding_button.pressed.connect(_begin_manual_capture.bind(role))
role_grid.add_child(binding_button)
_binding_buttons[role] = binding_button
var footer := HBoxContainer.new()
footer.alignment = BoxContainer.ALIGNMENT_END
footer.add_theme_constant_override("separation", 10)
page.add_child(footer)
_auto_map_button = Button.new()
_auto_map_button.text = "auto-map controller"
_auto_map_button.tooltip_text = (
"walk through every input on a standard xbox-style controller"
)
UtilityPageStyleType.apply_ocean_button(_auto_map_button)
_auto_map_button.pressed.connect(_begin_auto_map)
footer.add_child(_auto_map_button)
_reset_button = Button.new()
_reset_button.text = "restore defaults"
_reset_button.tooltip_text = "remove this controller's custom mapping"
UtilityPageStyleType.apply_ocean_button(_reset_button)
_reset_button.pressed.connect(_reset_mapping)
footer.add_child(_reset_button)
_close_button = Button.new()
_close_button.text = "done"
UtilityPageStyleType.apply_ocean_button(_close_button)
_close_button.pressed.connect(close_panel)
footer.add_child(_close_button)
func _begin_auto_map() -> void:
if _mapping_manager == null:
return
_auto_map_active = true
_auto_map_index = 0
_auto_map_draft = {}
_reset_cancel_combo()
_capture_device_id = _mapping_manager.get_active_device_id()
_set_capture_role(ControllerMappingManagerType.ROLE_ORDER[_auto_map_index])
func _begin_manual_capture(role: StringName) -> void:
_auto_map_active = false
_auto_map_index = -1
_auto_map_draft.clear()
_reset_cancel_combo()
_capture_device_id = _mapping_manager.get_active_device_id()
_set_capture_role(role)
func _set_capture_role(role: StringName) -> void:
_capturing_role = role
_waiting_for_neutral = true
_neutral_elapsed = 0.0
_set_action_buttons_disabled(true)
_progress_label.text = "release all controller inputs"
func _refresh_capture_prompt() -> void:
if _capturing_role.is_empty():
return
if _auto_map_active:
_progress_label.text = (
"step %d of %d: %s\n"
+ "hold both bumpers for %.2f seconds to cancel"
) % [
_auto_map_index + 1,
ControllerMappingManagerType.ROLE_ORDER.size(),
_mapping_manager.get_role_prompt(_capturing_role),
CANCEL_COMBO_HOLD_SECONDS,
]
return
var input_instruction: String = "press any controller button"
if _mapping_manager.role_expects_axis(_capturing_role):
input_instruction = "move any controller axis"
elif _mapping_manager.role_accepts_axis(_capturing_role):
input_instruction = "press any button or move any controller axis"
_progress_label.text = "%s for %s" % [
input_instruction,
_mapping_manager.get_role_label(_capturing_role),
]
func _accept_captured_binding(binding: Dictionary) -> void:
if _auto_map_active:
_auto_map_draft[str(_capturing_role)] = binding.duplicate(true)
_auto_map_index += 1
if _auto_map_index < ControllerMappingManagerType.ROLE_ORDER.size():
_set_capture_role(
ControllerMappingManagerType.ROLE_ORDER[_auto_map_index]
)
return
var profile_saved: bool = _mapping_manager.replace_active_bindings(
_auto_map_draft
)
_cancel_capture()
_progress_label.text = (
"controller mapped successfully"
if profile_saved else "could not save the controller mapping"
)
_refresh_bindings()
return
var role: StringName = _capturing_role
var binding_saved: bool = _mapping_manager.set_binding(role, binding)
_cancel_capture()
_progress_label.text = (
"updated " + _mapping_manager.get_role_label(role)
if binding_saved else "could not save that controller input"
)
_refresh_bindings()
func _cancel_capture() -> void:
_capturing_role = &""
_auto_map_active = false
_auto_map_index = -1
_auto_map_draft.clear()
_waiting_for_neutral = false
_neutral_elapsed = 0.0
_reset_cancel_combo()
_set_action_buttons_disabled(false)
if _progress_label != null:
_progress_label.text = ""
func _track_cancel_combo_button(button_event: InputEventJoypadButton) -> void:
if button_event == null:
return
match button_event.button_index:
JOY_BUTTON_LEFT_SHOULDER:
_cancel_left_bumper_pressed = button_event.pressed
JOY_BUTTON_RIGHT_SHOULDER:
_cancel_right_bumper_pressed = button_event.pressed
_:
return
if not (
_cancel_left_bumper_pressed and _cancel_right_bumper_pressed
):
_cancel_combo_elapsed = 0.0
func _reset_cancel_combo() -> void:
_cancel_left_bumper_pressed = false
_cancel_right_bumper_pressed = false
_cancel_combo_elapsed = 0.0
func _set_action_buttons_disabled(disabled: bool) -> void:
if _auto_map_button != null:
_auto_map_button.disabled = disabled
if _reset_button != null:
_reset_button.disabled = disabled
for button_value: Variant in _binding_buttons.values():
var button := button_value as Button
if button != null:
button.disabled = disabled
func _reset_mapping() -> void:
if _mapping_manager == null:
return
var reset: bool = _mapping_manager.reset_active_profile()
_progress_label.text = (
"restored the project controller defaults"
if reset else "could not restore controller defaults"
)
_refresh_bindings()
func _refresh_bindings() -> void:
if _mapping_manager == null or _controller_label == null:
return
_controller_label.text = _mapping_manager.get_active_controller_name()
var bindings: Dictionary = _mapping_manager.get_active_bindings()
for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
var button := _binding_buttons.get(role) as Button
if button == null:
continue
var raw_binding: Variant = bindings.get(str(role), {})
button.text = (
_mapping_manager.binding_label(raw_binding as Dictionary)
if typeof(raw_binding) == TYPE_DICTIONARY else "unmapped"
)
var conflict_role: StringName = _find_binding_conflict(
role,
raw_binding as Dictionary,
bindings,
)
_apply_binding_button_style(button, not conflict_role.is_empty())
button.tooltip_text = (
"warning: also assigned to "
+ _mapping_manager.get_role_label(conflict_role)
if not conflict_role.is_empty()
else "change " + _mapping_manager.get_role_label(role)
)
_reset_button.disabled = not _mapping_manager.has_custom_mapping()
func _find_binding_conflict(
role: StringName,
binding: Dictionary,
bindings: Dictionary,
) -> StringName:
if binding.is_empty():
return &""
for other_role: StringName in ControllerMappingManagerType.ROLE_ORDER:
if other_role == role:
continue
var raw_other: Variant = bindings.get(str(other_role), {})
if typeof(raw_other) != TYPE_DICTIONARY:
continue
var other := raw_other as Dictionary
if _bindings_conflict(role, binding, other_role, other):
return other_role
return &""
func _bindings_conflict(
first_role: StringName,
first: Dictionary,
second_role: StringName,
second: Dictionary,
) -> bool:
var binding_kind: String = str(first.get("kind", ""))
if binding_kind != str(second.get("kind", "")):
return false
if binding_kind == "button":
return int(first.get("button", -1)) == int(
second.get("button", -1)
)
if binding_kind != "axis":
return false
if int(first.get("axis", -1)) != int(second.get("axis", -1)):
return false
var opposite_trigger_halves: bool = (
first_role in ControllerMappingManagerType.TRIGGER_ROLES
and second_role in ControllerMappingManagerType.TRIGGER_ROLES
and signf(float(first.get("direction", 0.0)))
!= signf(float(second.get("direction", 0.0)))
)
return not opposite_trigger_halves
func _apply_binding_button_style(button: Button, has_conflict: bool) -> void:
UtilityPageStyleType.apply_compact_ocean_button(button)
if not has_conflict:
return
var colors: Dictionary = {
&"normal": UtilityPageStyleType.OCEAN_DANGER,
&"hover": UtilityPageStyleType.OCEAN_DANGER.lightened(0.12),
&"pressed": UtilityPageStyleType.OCEAN_DANGER.darkened(0.12),
&"focus": UtilityPageStyleType.OCEAN_DANGER.lightened(0.12),
}
for state: StringName in colors:
var style := UtilityPageStyleType.ocean_button_style(
colors[state] as Color
)
style.content_margin_left = 10.0
style.content_margin_right = 10.0
style.content_margin_top = 4.0
style.content_margin_bottom = 4.0
button.add_theme_stylebox_override(state, style)
func _on_active_controller_changed(_controller_name: String) -> void:
if not _capturing_role.is_empty():
_cancel_capture()
_refresh_bindings()