536 lines
16 KiB
GDScript
536 lines
16 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
|
|
|
|
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
|
|
|
|
|
|
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 _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
|
|
if (
|
|
button_event != null
|
|
and button_event.pressed
|
|
and _capturing_role != ControllerMappingManagerType.ROLE_B
|
|
and _mapping_manager.event_matches_role(
|
|
event,
|
|
ControllerMappingManagerType.ROLE_B,
|
|
)
|
|
):
|
|
_cancel_capture()
|
|
_progress_label.text = "controller mapping cancelled"
|
|
return
|
|
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 = {}
|
|
_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()
|
|
_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" % [
|
|
_auto_map_index + 1,
|
|
ControllerMappingManagerType.ROLE_ORDER.size(),
|
|
_mapping_manager.get_role_prompt(_capturing_role),
|
|
]
|
|
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 saved: bool = _mapping_manager.replace_active_bindings(
|
|
_auto_map_draft
|
|
)
|
|
_cancel_capture()
|
|
_progress_label.text = (
|
|
"controller mapped successfully"
|
|
if saved else "could not save the controller mapping"
|
|
)
|
|
_refresh_bindings()
|
|
return
|
|
var role: StringName = _capturing_role
|
|
var saved: bool = _mapping_manager.set_binding(role, binding)
|
|
_cancel_capture()
|
|
_progress_label.text = (
|
|
"updated " + _mapping_manager.get_role_label(role)
|
|
if 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
|
|
_set_action_buttons_disabled(false)
|
|
if _progress_label != null:
|
|
_progress_label.text = ""
|
|
|
|
|
|
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()
|